504 lines
16 KiB
Markdown
504 lines
16 KiB
Markdown
|
|
# P0: Skill-Scoped 密钥管理 实施计划
|
|||
|
|
|
|||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|||
|
|
|
|||
|
|
**Goal:** 让每个 Skill 拥有独立的密钥/配置管理,通过 DB 加密存储,前端可视化配置,运行时自动注入到 skill 子进程环境变量。
|
|||
|
|
|
|||
|
|
**Architecture:** 新增 `SkillSecretService` 负责 AES-256-GCM 加密/解密。`netaclaw_skill` 表新增 `secrets`(加密 TEXT)和 `envSchema`(JSON 声明)字段。Admin controller 暴露配置端点,前端 skill-detail 抽屉新增配置 tab。bash 工具重构支持 env override,运行 skill 目录下脚本时自动注入 skill-scoped env。
|
|||
|
|
|
|||
|
|
**Tech Stack:** Node.js crypto (AES-256-GCM), TypeORM, Midway.js DI, Element Plus (前端)
|
|||
|
|
|
|||
|
|
**Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 3 + 10.2-10.4
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1: Entity 字段扩展
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/entity/skill.ts`
|
|||
|
|
- Modify: `packages/shared/types/skill.types.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 在 entity/skill.ts 新增 secrets 和 envSchema 字段**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 在 fingerprint 字段之后添加
|
|||
|
|
|
|||
|
|
@Column({ type: 'text', comment: 'AES-256-GCM 加密的 secrets JSON', nullable: true })
|
|||
|
|
secrets: string;
|
|||
|
|
|
|||
|
|
@Column({ type: 'json', comment: 'env 声明 schema', nullable: true })
|
|||
|
|
envSchema: Array<{ name: string; required: boolean; description?: string; default?: string }>;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 在 shared/types/skill.types.ts 新增 EnvSchemaItem 类型**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export interface EnvSchemaItem {
|
|||
|
|
name: string;
|
|||
|
|
required: boolean;
|
|||
|
|
description?: string;
|
|||
|
|
default?: string;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 重启开发服务器验证 TypeORM 自动同步**
|
|||
|
|
|
|||
|
|
Run: 重启 backend,检查 `netaclaw_skill` 表是否新增了 `secrets` 和 `envSchema` 列。可通过 MCP mysql `describe_table` 工具验证。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/entity/skill.ts packages/shared/types/skill.types.ts
|
|||
|
|
git commit -m "feat(skill): add secrets and envSchema columns to netaclaw_skill entity"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2: SkillSecretService 实现
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `packages/backend/src/modules/netaclaw/service/skill_secret.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建 skill_secret.ts 文件,实现加密/解密**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { Provide, Scope, ScopeEnum, Logger, Init } from '@midwayjs/core';
|
|||
|
|
import { ILogger } from '@midwayjs/logger';
|
|||
|
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|||
|
|
import { Repository } from 'typeorm';
|
|||
|
|
import * as crypto from 'crypto';
|
|||
|
|
import { NetaClawSkillEntity } from '../entity/skill.js';
|
|||
|
|
|
|||
|
|
@Provide()
|
|||
|
|
@Scope(ScopeEnum.Singleton)
|
|||
|
|
export class SkillSecretService {
|
|||
|
|
@Logger()
|
|||
|
|
logger: ILogger;
|
|||
|
|
|
|||
|
|
@InjectEntityModel(NetaClawSkillEntity)
|
|||
|
|
skillRepo: Repository<NetaClawSkillEntity>;
|
|||
|
|
|
|||
|
|
private readonly algorithm = 'aes-256-gcm' as const;
|
|||
|
|
private readonly ivLength = 16;
|
|||
|
|
private readonly authTagLength = 16;
|
|||
|
|
|
|||
|
|
private deriveKey(): Buffer {
|
|||
|
|
const raw = process.env.SKILL_SECRET_KEY || process.env.APP_SECRET;
|
|||
|
|
if (!raw) throw new Error('SKILL_SECRET_KEY or APP_SECRET environment variable must be set');
|
|||
|
|
return crypto.createHash('sha256').update(raw).digest();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
encrypt(plainObj: Record<string, string>): string {
|
|||
|
|
const iv = crypto.randomBytes(this.ivLength);
|
|||
|
|
const key = this.deriveKey();
|
|||
|
|
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
|
|||
|
|
const encrypted = Buffer.concat([
|
|||
|
|
cipher.update(JSON.stringify(plainObj), 'utf8'),
|
|||
|
|
cipher.final(),
|
|||
|
|
]);
|
|||
|
|
const authTag = cipher.getAuthTag();
|
|||
|
|
return Buffer.concat([iv, encrypted, authTag]).toString('base64');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
decrypt(cipherText: string): Record<string, string> {
|
|||
|
|
const buf = Buffer.from(cipherText, 'base64');
|
|||
|
|
const iv = buf.subarray(0, this.ivLength);
|
|||
|
|
const authTag = buf.subarray(buf.length - this.authTagLength);
|
|||
|
|
const encrypted = buf.subarray(this.ivLength, buf.length - this.authTagLength);
|
|||
|
|
const key = this.deriveKey();
|
|||
|
|
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
|
|||
|
|
decipher.setAuthTag(authTag);
|
|||
|
|
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|||
|
|
return JSON.parse(decrypted.toString('utf8'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async resolveEnv(skillName: string): Promise<Record<string, string>> {
|
|||
|
|
const entity = await this.skillRepo.findOneBy({ name: skillName });
|
|||
|
|
if (!entity) return {};
|
|||
|
|
|
|||
|
|
const env: Record<string, string> = {};
|
|||
|
|
|
|||
|
|
// 填充 defaults
|
|||
|
|
if (entity.envSchema) {
|
|||
|
|
for (const item of entity.envSchema) {
|
|||
|
|
if (item.default) env[item.name] = item.default;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 覆盖 secrets
|
|||
|
|
if (entity.secrets) {
|
|||
|
|
try {
|
|||
|
|
const secrets = this.decrypt(entity.secrets);
|
|||
|
|
Object.assign(env, secrets);
|
|||
|
|
} catch (e) {
|
|||
|
|
this.logger.error('[SkillSecret] 解密 %s secrets 失败: %s', skillName, e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return env;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async saveSecrets(skillName: string, secrets: Record<string, string>): Promise<void> {
|
|||
|
|
const encrypted = this.encrypt(secrets);
|
|||
|
|
await this.skillRepo.update({ name: skillName }, { secrets: encrypted });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getConfiguredKeys(skillName: string): Promise<Array<{ name: string; hasValue: boolean }>> {
|
|||
|
|
const entity = await this.skillRepo.findOneBy({ name: skillName });
|
|||
|
|
if (!entity?.envSchema) return [];
|
|||
|
|
|
|||
|
|
let configuredKeys = new Set<string>();
|
|||
|
|
if (entity.secrets) {
|
|||
|
|
try {
|
|||
|
|
const secrets = this.decrypt(entity.secrets);
|
|||
|
|
configuredKeys = new Set(Object.keys(secrets));
|
|||
|
|
} catch { /* ignore */ }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return entity.envSchema.map(item => ({
|
|||
|
|
name: item.name,
|
|||
|
|
hasValue: configuredKeys.has(item.name),
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译通过**
|
|||
|
|
|
|||
|
|
Run: `cd packages/backend && npx tsc --noEmit`
|
|||
|
|
Expected: 无错误
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/service/skill_secret.ts
|
|||
|
|
git commit -m "feat(skill): add SkillSecretService with AES-256-GCM encryption"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 3: Admin Controller 端点
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/controller/admin/skill.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 在 AdminNetaClawSkillController 中注入 SkillSecretService**
|
|||
|
|
|
|||
|
|
在文件顶部 import 区域添加:
|
|||
|
|
```typescript
|
|||
|
|
import { SkillSecretService } from '../../service/skill_secret.js';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
在 class 内部添加注入:
|
|||
|
|
```typescript
|
|||
|
|
@Inject()
|
|||
|
|
skillSecret: SkillSecretService;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 新增 envSchema 端点**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@Get('/envSchema', { summary: '获取 skill 的 env 声明和配置状态' })
|
|||
|
|
async envSchema(@Query('name') name: string) {
|
|||
|
|
if (!name) return this.fail('缺少 name 参数');
|
|||
|
|
const keys = await this.skillSecret.getConfiguredKeys(name);
|
|||
|
|
const skill = await this.skillLoader.getSkill(name);
|
|||
|
|
const schema = skill?.metadata?.env || [];
|
|||
|
|
return this.ok({ name, schema, configured: keys });
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 新增 secrets 保存端点**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@Post('/secrets', { summary: '保存 skill secrets(加密存储)' })
|
|||
|
|
async saveSecrets(@Body() body: { name: string; secrets: Record<string, string> }) {
|
|||
|
|
if (!body.name || !body.secrets) return this.fail('缺少 name 或 secrets');
|
|||
|
|
try {
|
|||
|
|
await this.skillSecret.saveSecrets(body.name, body.secrets);
|
|||
|
|
return this.ok();
|
|||
|
|
} catch (e: any) {
|
|||
|
|
return this.fail(e.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 验证编译通过**
|
|||
|
|
|
|||
|
|
Run: `cd packages/backend && npx tsc --noEmit`
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/controller/admin/skill.ts
|
|||
|
|
git commit -m "feat(skill): add envSchema and secrets admin endpoints"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 4: SkillLoaderService 新增 resolveSkillByPath
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 在 SkillLoaderService 中新增 resolveSkillByPath 方法**
|
|||
|
|
|
|||
|
|
在 class 末尾(`getSkillFilePath` 方法之后)添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
/** 根据绝对路径判断是否属于某个 skill 目录,返回 skill name 或 null */
|
|||
|
|
resolveSkillByPath(absPath: string): string | null {
|
|||
|
|
if (!absPath) return null;
|
|||
|
|
const normalized = path.resolve(absPath);
|
|||
|
|
const skillsDirNorm = path.resolve(this.skillsDir);
|
|||
|
|
if (!normalized.startsWith(skillsDirNorm + path.sep)) return null;
|
|||
|
|
const relative = normalized.slice(skillsDirNorm.length + 1);
|
|||
|
|
const skillName = relative.split(path.sep)[0];
|
|||
|
|
if (!skillName || !this.skills.has(skillName)) return null;
|
|||
|
|
return skillName;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译通过**
|
|||
|
|
|
|||
|
|
Run: `cd packages/backend && npx tsc --noEmit`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
|||
|
|
git commit -m "feat(skill): add resolveSkillByPath for skill directory detection"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 5: Bash 工具 env 注入重构
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/tools/builtin/bash.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 新增 BashEnvProvider 接口并修改 createLocalBashOperations 签名**
|
|||
|
|
|
|||
|
|
在 `BashToolOptions` 接口之前添加:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export interface BashEnvProvider {
|
|||
|
|
getAdditionalEnv(cwd: string): Promise<Record<string, string>>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
修改 `createLocalBashOperations` 签名:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export function createLocalBashOperations(envProvider?: BashEnvProvider): BashOperations {
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 在 exec 方法中注入额外 env**
|
|||
|
|
|
|||
|
|
修改 `exec` 方法内部,在 spawn 之前获取额外 env:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
exec: async (command, cwd, options) => {
|
|||
|
|
const shellConfig = resolveShellConfig();
|
|||
|
|
const shell = shellConfig.shell;
|
|||
|
|
const shellArgs = [...shellConfig.args, command];
|
|||
|
|
|
|||
|
|
// 获取 skill-scoped env
|
|||
|
|
let envVars: Record<string, string> = { ...process.env } as any;
|
|||
|
|
if (envProvider) {
|
|||
|
|
try {
|
|||
|
|
const additional = await envProvider.getAdditionalEnv(cwd);
|
|||
|
|
Object.assign(envVars, additional);
|
|||
|
|
} catch { /* ignore env resolution failures */ }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new Promise(async (resolve, reject) => {
|
|||
|
|
const child = await spawnWithFallback(shell, shellArgs, {
|
|||
|
|
cwd,
|
|||
|
|
env: envVars,
|
|||
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|||
|
|
...withHiddenWindow({}),
|
|||
|
|
}).catch(reject);
|
|||
|
|
// ... rest unchanged
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 找到 createLocalBashOperations 的调用点,传入 envProvider**
|
|||
|
|
|
|||
|
|
搜索 `createLocalBashOperations()` 的调用位置(通常在 bash 工具的工厂函数或 tool_resolver 中),将 `SkillSecretService` + `SkillLoaderService` 组合为 `BashEnvProvider` 传入。
|
|||
|
|
|
|||
|
|
实现一个适配器:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 在 bash.ts 底部或单独文件
|
|||
|
|
export function createSkillBashEnvProvider(
|
|||
|
|
skillLoader: SkillLoaderService,
|
|||
|
|
skillSecret: SkillSecretService,
|
|||
|
|
): BashEnvProvider {
|
|||
|
|
return {
|
|||
|
|
async getAdditionalEnv(cwd: string): Promise<Record<string, string>> {
|
|||
|
|
const skillName = skillLoader.resolveSkillByPath(cwd);
|
|||
|
|
if (!skillName) return {};
|
|||
|
|
return skillSecret.resolveEnv(skillName);
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3.5: 在 tool_resolver.ts 中接线 BashEnvProvider**
|
|||
|
|
|
|||
|
|
找到 `tool_resolver.ts` 中创建 bash 工具的位置(搜索 `createBashTool` 或 `createLocalBashOperations`)。将 `SkillLoaderService` 和 `SkillSecretService` 组合为 `BashEnvProvider` 传入:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// tool_resolver.ts 中
|
|||
|
|
import { createSkillBashEnvProvider } from '../tools/builtin/bash.js';
|
|||
|
|
import { SkillSecretService } from './skill_secret.js';
|
|||
|
|
|
|||
|
|
// class 内部注入
|
|||
|
|
@Inject()
|
|||
|
|
skillSecret: SkillSecretService;
|
|||
|
|
|
|||
|
|
// 在 bash 工具创建处传入 envProvider
|
|||
|
|
const bashEnvProvider = createSkillBashEnvProvider(this.skillLoader, this.skillSecret);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
具体接线方式取决于 bash 工具的创建路径——在实施时需要 trace `createLocalBashOperations()` 的调用链,将 `bashEnvProvider` 参数传递到位。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 验证编译通过(bash.ts)**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 6: 前端 skill-detail 配置 Tab
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/frontend/src/modules/agent/components/skill-detail.vue`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 重构 skill-detail.vue 为 tab 布局**
|
|||
|
|
|
|||
|
|
将现有内容包裹在 `<el-tabs>` 中,第一个 tab 保留原有内容(基本信息 + SKILL.md),新增第二个 tab "配置":
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<el-tabs v-model="activeTab">
|
|||
|
|
<el-tab-pane label="基本信息" name="info">
|
|||
|
|
<!-- 现有内容移入此处 -->
|
|||
|
|
</el-tab-pane>
|
|||
|
|
<el-tab-pane label="配置" name="config">
|
|||
|
|
<div v-if="envSchema.length === 0" class="empty-hint">此 Skill 无需配置环境变量</div>
|
|||
|
|
<div v-else>
|
|||
|
|
<div v-for="item in envSchema" :key="item.name" class="env-row">
|
|||
|
|
<div class="env-label">
|
|||
|
|
<span>{{ item.name }}</span>
|
|||
|
|
<el-tag v-if="item.required" size="small" type="danger">必填</el-tag>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="item.description" class="env-desc">{{ item.description }}</div>
|
|||
|
|
<el-input
|
|||
|
|
v-model="secretValues[item.name]"
|
|||
|
|
:placeholder="getConfiguredStatus(item.name) ? '已配置(留空则不修改)' : item.default || '请输入'"
|
|||
|
|
show-password
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<el-button type="primary" style="margin-top: 16px" @click="handleSaveSecrets">保存配置</el-button>
|
|||
|
|
</div>
|
|||
|
|
</el-tab-pane>
|
|||
|
|
</el-tabs>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 新增配置相关的 script 逻辑**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const activeTab = ref('info');
|
|||
|
|
const envSchema = ref<any[]>([]);
|
|||
|
|
const configuredKeys = ref<any[]>([]);
|
|||
|
|
const secretValues = ref<Record<string, string>>({});
|
|||
|
|
|
|||
|
|
function getConfiguredStatus(name: string): boolean {
|
|||
|
|
return configuredKeys.value.find(k => k.name === name)?.hasValue || false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadEnvSchema() {
|
|||
|
|
if (!props.skill?.name) return;
|
|||
|
|
try {
|
|||
|
|
const res = await service.request({
|
|||
|
|
url: '/admin/netaclaw/skill/envSchema',
|
|||
|
|
params: { name: props.skill.name },
|
|||
|
|
});
|
|||
|
|
envSchema.value = res?.schema || [];
|
|||
|
|
configuredKeys.value = res?.configured || [];
|
|||
|
|
} catch { /* ignore */ }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function handleSaveSecrets() {
|
|||
|
|
const nonEmpty = Object.fromEntries(
|
|||
|
|
Object.entries(secretValues.value).filter(([_, v]) => v.trim())
|
|||
|
|
);
|
|||
|
|
if (Object.keys(nonEmpty).length === 0) {
|
|||
|
|
ElMessage.warning('请至少填写一个配置项');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
await service.request({
|
|||
|
|
url: '/admin/netaclaw/skill/secrets',
|
|||
|
|
method: 'POST',
|
|||
|
|
data: { name: props.skill.name, secrets: nonEmpty },
|
|||
|
|
});
|
|||
|
|
ElMessage.success('配置已保存');
|
|||
|
|
secretValues.value = {};
|
|||
|
|
await loadEnvSchema();
|
|||
|
|
} catch (e: any) {
|
|||
|
|
ElMessage.error(e.message || '保存失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(() => props.skill, () => {
|
|||
|
|
activeTab.value = 'info';
|
|||
|
|
secretValues.value = {};
|
|||
|
|
loadEnvSchema();
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 验证前端编译通过**
|
|||
|
|
|
|||
|
|
Run: `cd packages/frontend && npx vue-tsc --noEmit`
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/frontend/src/modules/agent/components/skill-detail.vue
|
|||
|
|
git commit -m "feat(skill): add configuration tab to skill-detail drawer"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 7: SkillLoaderService 解析 envSchema 并同步到 DB
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 在 scanSkills 中解析 metadata.env 并同步 envSchema 到 DB**
|
|||
|
|
|
|||
|
|
在 `getSkillMetas()` 方法中,当同步到 DB 时,同时写入 envSchema:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 在 getSkillMetas() 的 skillRepo.save 调用中,添加 envSchema 字段
|
|||
|
|
const envFromMeta = (fs.metadata as any)?.env;
|
|||
|
|
const envSchema = Array.isArray(envFromMeta) ? envFromMeta.map((e: any) => ({
|
|||
|
|
name: e.name,
|
|||
|
|
required: !!e.required,
|
|||
|
|
description: e.description || undefined,
|
|||
|
|
default: e.default || undefined,
|
|||
|
|
})) : null;
|
|||
|
|
|
|||
|
|
// 在 save/update 的 entityData 中加入
|
|||
|
|
envSchema,
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译通过**
|
|||
|
|
|
|||
|
|
Run: `cd packages/backend && npx tsc --noEmit`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
|||
|
|
git commit -m "feat(skill): sync envSchema from SKILL.md metadata to DB during scan"
|
|||
|
|
```
|