openfeel 1.0.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +11 -11
- package/README.md +2 -1
- package/README.zh-CN.md +13 -13
- package/bin/openfeel.js +2 -2
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +28 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/archive.d.ts +1 -1
- package/dist/commands/flow.d.ts +1 -1
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +20 -8
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts +10 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +106 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/model.d.ts +10 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +135 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/setup.d.ts +7 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +31 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/stage.d.ts.map +1 -1
- package/dist/commands/stage.js +50 -29
- package/dist/commands/stage.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +13 -10
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.d.ts.map +1 -1
- package/dist/commands/view.js +2 -1
- package/dist/commands/view.js.map +1 -1
- package/dist/core/archive/merge.d.ts.map +1 -1
- package/dist/core/archive/merge.js +5 -4
- package/dist/core/archive/merge.js.map +1 -1
- package/dist/core/artifact-graph/resolver.js +2 -2
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/config.d.ts +8 -8
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +22 -17
- package/dist/core/config.js.map +1 -1
- package/dist/core/flow-manager.d.ts +25 -4
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +144 -79
- package/dist/core/flow-manager.js.map +1 -1
- package/dist/core/fs/atomic-write.d.ts +31 -0
- package/dist/core/fs/atomic-write.d.ts.map +1 -0
- package/dist/core/fs/atomic-write.js +80 -0
- package/dist/core/fs/atomic-write.js.map +1 -0
- package/dist/core/fs/file-lock.d.ts +34 -0
- package/dist/core/fs/file-lock.d.ts.map +1 -0
- package/dist/core/fs/file-lock.js +142 -0
- package/dist/core/fs/file-lock.js.map +1 -0
- package/dist/core/fs/sequence.d.ts +42 -0
- package/dist/core/fs/sequence.d.ts.map +1 -0
- package/dist/core/fs/sequence.js +69 -0
- package/dist/core/fs/sequence.js.map +1 -0
- package/dist/core/global-paths.d.ts +27 -0
- package/dist/core/global-paths.d.ts.map +1 -0
- package/dist/core/global-paths.js +61 -0
- package/dist/core/global-paths.js.map +1 -0
- package/dist/core/i18n-data/en.d.ts +3 -0
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +86 -1
- package/dist/core/i18n-data/en.js.map +1 -1
- package/dist/core/i18n-data/zh-CN.d.ts +3 -0
- package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
- package/dist/core/i18n-data/zh-CN.js +87 -1
- package/dist/core/i18n-data/zh-CN.js.map +1 -1
- package/dist/core/i18n.js +4 -4
- package/dist/core/i18n.js.map +1 -1
- package/dist/core/init.d.ts +10 -15
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +65 -134
- package/dist/core/init.js.map +1 -1
- package/dist/core/managed-region.d.ts +54 -0
- package/dist/core/managed-region.d.ts.map +1 -0
- package/dist/core/managed-region.js +171 -0
- package/dist/core/managed-region.js.map +1 -0
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +6 -3
- package/dist/core/metrics.js.map +1 -1
- package/dist/core/migrate.d.ts +115 -0
- package/dist/core/migrate.d.ts.map +1 -0
- package/dist/core/migrate.js +558 -0
- package/dist/core/migrate.js.map +1 -0
- package/dist/core/model-config.d.ts +62 -0
- package/dist/core/model-config.d.ts.map +1 -0
- package/dist/core/model-config.js +336 -0
- package/dist/core/model-config.js.map +1 -0
- package/dist/core/opencode-config.d.ts +17 -0
- package/dist/core/opencode-config.d.ts.map +1 -0
- package/dist/core/opencode-config.js +135 -0
- package/dist/core/opencode-config.js.map +1 -0
- package/dist/core/plan/roadmap.d.ts.map +1 -1
- package/dist/core/plan/roadmap.js +3 -2
- package/dist/core/plan/roadmap.js.map +1 -1
- package/dist/core/plan/scheme.d.ts.map +1 -1
- package/dist/core/plan/scheme.js +35 -36
- package/dist/core/plan/scheme.js.map +1 -1
- package/dist/core/plan/stage.d.ts.map +1 -1
- package/dist/core/plan/stage.js +4 -3
- package/dist/core/plan/stage.js.map +1 -1
- package/dist/core/public-logger.d.ts +3 -3
- package/dist/core/public-logger.d.ts.map +1 -1
- package/dist/core/public-logger.js +36 -40
- package/dist/core/public-logger.js.map +1 -1
- package/dist/core/setup.d.ts +10 -0
- package/dist/core/setup.d.ts.map +1 -0
- package/dist/core/setup.js +75 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/template-loader.d.ts +4 -5
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +3974 -3274
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/templates.d.ts +2 -2
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +2 -2
- package/dist/core/templates.js.map +1 -1
- package/dist/core/update-infos.d.ts +32 -0
- package/dist/core/update-infos.d.ts.map +1 -0
- package/dist/core/update-infos.js +153 -0
- package/dist/core/update-infos.js.map +1 -0
- package/dist/core/update-state.d.ts +18 -0
- package/dist/core/update-state.d.ts.map +1 -1
- package/dist/core/update-state.js +66 -3
- package/dist/core/update-state.js.map +1 -1
- package/dist/core/update.d.ts +12 -6
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +400 -550
- package/dist/core/update.js.map +1 -1
- package/dist/core/view/entry.js +4 -4
- package/dist/core/view/entry.js.map +1 -1
- package/dist/core/workspace/identity.d.ts.map +1 -1
- package/dist/core/workspace/identity.js +13 -10
- package/dist/core/workspace/identity.js.map +1 -1
- package/dist/core/workspace/knowledge.d.ts.map +1 -1
- package/dist/core/workspace/knowledge.js +24 -14
- package/dist/core/workspace/knowledge.js.map +1 -1
- package/package.json +1 -1
package/dist/core/update.js
CHANGED
|
@@ -1,34 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode 适配器更新编排
|
|
3
|
-
*
|
|
3
|
+
* 增量部署全局资产(全局 AGENTS.md、agent、skill)并更新全局/项目 opencode.jsonc 配置。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - 新增 instructions/core.md 创建(从 init.ts
|
|
5
|
+
* 变更摘要:
|
|
6
|
+
* - v3-stage-04: 新增 instructions/core.md 创建(从 init.ts 迁移)
|
|
7
|
+
* - v1.1.1: 拆除项目 AGENTS.md 部署;全局约束由 core.md 改为全局 AGENTS.md(收归 openfeel setup)
|
|
7
8
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { resolve, dirname, join } from 'node:path';
|
|
9
|
+
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { resolve, dirname, join, basename, isAbsolute, relative } from 'node:path';
|
|
10
11
|
import { loadAgentTemplate, listAgentIds, loadTemplate } from './template-loader.js';
|
|
11
|
-
import { recordProjectLang
|
|
12
|
+
import { recordProjectLang } from './workspace/identity.js';
|
|
12
13
|
import { getCliLang } from './i18n.js';
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.projectLang = projectLang;
|
|
22
|
-
this.requestedLang = requestedLang;
|
|
23
|
-
this.name = 'AgentsMdLangConflictError';
|
|
24
|
-
}
|
|
25
|
-
}
|
|
14
|
+
import { getOpencodeGlobalDir, getGlobalAgentsDir, getGlobalSkillsDir, getGlobalCoreMdPath, getGlobalAgentsMdPath, getGlobalOpencodeJsoncPath, getGlobalUpdateStatePath } from './global-paths.js';
|
|
15
|
+
import { mergeGlobalOpencodeJsonc, buildProjectOpencodeJsoncObj } from './opencode-config.js';
|
|
16
|
+
import { atomicWriteFileSync } from './fs/atomic-write.js';
|
|
17
|
+
import { withFileLock, globalLockPath } from './fs/file-lock.js';
|
|
18
|
+
import { detectFileType, normalize, splitFrontmatter, parseRegion, wrapRegion, replaceRegion, mergeFrontmatter, serializeFrontmatter } from './managed-region.js';
|
|
19
|
+
import { appendUpdateInfo } from './update-infos.js';
|
|
20
|
+
// 新增:update_state.json hash 比对与冲突检测(项目 state + 全局 state)
|
|
21
|
+
import { hashContent, loadUpdateState, saveUpdateState, createUpdateState, loadGlobalUpdateState, saveGlobalUpdateState, createGlobalUpdateState, updateFileHash, markFileConflict, getOpenfeelVersion, } from './update-state.js';
|
|
26
22
|
/** 当前支持部署的 AI 工具列表 */
|
|
27
23
|
export const supportedTools = [
|
|
28
24
|
{
|
|
29
25
|
id: 'opencode',
|
|
30
26
|
name: 'OpenCode',
|
|
31
|
-
description: 'OpenCode AI 编码助手 — 生成 Agent 定义和
|
|
27
|
+
description: 'OpenCode AI 编码助手 — 生成 Agent 定义和 openfeel-* 技能',
|
|
32
28
|
enabled: true,
|
|
33
29
|
},
|
|
34
30
|
// 后续扩展:
|
|
@@ -64,13 +60,13 @@ export async function selectTools() {
|
|
|
64
60
|
// ─── Agent 定义由 template-loader.ts 统一管理 ──────────────────────────
|
|
65
61
|
// ─── Skill 定义内容 ─────────────────────────────────────────────────
|
|
66
62
|
// AUTO-GENERATED-BEGIN: SKILL_DEFINITIONS
|
|
67
|
-
const SKILL_DEFINITIONS = {
|
|
68
|
-
'agent-model-check': `---
|
|
69
|
-
name: agent-model-check
|
|
70
|
-
description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或需要排查模型配置时使用。涵盖 auth.json 校验、provider key 匹配、模型能力确认、
|
|
63
|
+
export const SKILL_DEFINITIONS = {
|
|
64
|
+
'openfeel-agent-model-check': `---
|
|
65
|
+
name: openfeel-agent-model-check
|
|
66
|
+
description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或需要排查模型配置时使用。涵盖 auth.json 校验、provider key 匹配、模型能力确认、openfeel-vision 多模态专项指南。
|
|
71
67
|
---
|
|
72
68
|
|
|
73
|
-
# Skill: agent-model-check
|
|
69
|
+
# Skill: openfeel-agent-model-check
|
|
74
70
|
|
|
75
71
|
# Agent 模型检查与修复
|
|
76
72
|
|
|
@@ -79,7 +75,7 @@ description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或
|
|
|
79
75
|
- Agent 调度时报 \`Model not found: xxx\`
|
|
80
76
|
- 需要验证某个 Agent 的模型是否可用
|
|
81
77
|
- 新增 Agent 后需确认模型配置正确
|
|
82
|
-
- 排查多模态(
|
|
78
|
+
- 排查多模态(openfeel-vision)Agent 无法处理图片的问题
|
|
83
79
|
|
|
84
80
|
## 排查流程
|
|
85
81
|
|
|
@@ -113,22 +109,22 @@ cat ~/.local/share/opencode/auth.json
|
|
|
113
109
|
|
|
114
110
|
| 能力需求 | 需确认的字段 | 示例 |
|
|
115
111
|
|----------|-------------|------|
|
|
116
|
-
| 视觉/图像分析 | Input = Yes | \`
|
|
112
|
+
| 视觉/图像分析 | Input = Yes | \`deepseek-flash\` |
|
|
117
113
|
| 工具调用 | Tool Call = Yes | \`qwen3.7-plus\` |
|
|
118
114
|
| 结构化输出 | Structured = Yes | \`qwen3.7-flash\` |
|
|
119
115
|
| 推理/思考 | Reasoning = Yes | \`qwq-plus\` |
|
|
120
116
|
|
|
121
117
|
**常见陷阱**:
|
|
122
118
|
- \`qwen3.7-plus\` 是纯文本模型,不支持图像输入
|
|
123
|
-
- \`
|
|
124
|
-
-
|
|
119
|
+
- \`deepseek-flash\` 支持多模态图像输入(openfeel-vision 现用模型)
|
|
120
|
+
- 备选视觉模型:\`qwen3-vl-plus\`(模型名中的 \`vl\` 表示 Vision-Language)
|
|
125
121
|
|
|
126
122
|
### 第四步:检查 opencode.jsonc 配置
|
|
127
123
|
|
|
128
124
|
\`\`\`jsonc
|
|
129
125
|
{
|
|
130
126
|
"agent": {
|
|
131
|
-
"vision": {
|
|
127
|
+
"openfeel-vision": {
|
|
132
128
|
"model": "{auth.json_key}/{model_id}" // 格式:provider_key/model_id
|
|
133
129
|
}
|
|
134
130
|
}
|
|
@@ -159,17 +155,18 @@ cat ~/.local/share/opencode/auth.json
|
|
|
159
155
|
| opencode.jsonc 语法 | 检查 JSON 格式 | 无语法错误 |
|
|
160
156
|
| 重启生效 | 重启 opencode 后重新测试 | 无 Model not found 报错 |
|
|
161
157
|
|
|
162
|
-
## 多模态(
|
|
158
|
+
## 多模态(openfeel-vision)Agent 专项
|
|
163
159
|
|
|
164
|
-
|
|
160
|
+
openfeel-vision Agent 必须配置多模态模型。当前默认使用 \`deepseek/deepseek-flash\`(已支持多模态图像输入):
|
|
165
161
|
|
|
166
|
-
| 模型 ID |
|
|
167
|
-
|
|
168
|
-
|
|
|
162
|
+
| 模型 ID | 完整引用 | 上下文 | 图像输入 |
|
|
163
|
+
|---------|---------|--------|---------|
|
|
164
|
+
| deepseek-flash | \`deepseek/deepseek-flash\` | — | ✅ |
|
|
165
|
+
| qwen3-vl-plus | \`alibaba-cn/qwen3-vl-plus\` | 262K | ✅(备选) |
|
|
169
166
|
| qwen-vl-plus | \`alibaba-cn/qwen-vl-plus\` | 131K | ❌(旧版) |
|
|
170
167
|
| qwen-vl-max | \`alibaba-cn/qwen-vl-max\` | 131K | ❌(旧版) |
|
|
171
168
|
|
|
172
|
-
**推荐**:优先使用 \`qwen3-vl-plus
|
|
169
|
+
**推荐**:优先使用 \`deepseek-flash\`(已内置多模态);如需更大上下文可备选 \`qwen3-vl-plus\`。
|
|
173
170
|
|
|
174
171
|
## 常见错误与修复
|
|
175
172
|
|
|
@@ -178,10 +175,10 @@ Vision Agent 必须配置多模态模型。Alibaba 系列视觉模型:
|
|
|
178
175
|
| \`Model not found: Alibaba(China)/xxx\` | 使用了自定义 provider name 而非 auth.json key | 改为 auth.json 中的实际 key |
|
|
179
176
|
| \`Model not found: alibaba/xxx\` | 内置 key 与实际注册的 key 不一致 | 检查 auth.json,使用实际 key |
|
|
180
177
|
| \`Model not found: xxx. Did you mean: yyy\` | 模型名拼写错误或不存在 | 使用 \`Did you mean\` 建议的名称 |
|
|
181
|
-
| Agent 调度成功但无法处理图片 | 配置了纯文本模型 |
|
|
178
|
+
| Agent 调度成功但无法处理图片 | 配置了纯文本模型 | 改为支持图像输入的模型(如 \`deepseek-flash\`) |
|
|
182
179
|
`,
|
|
183
|
-
'bug-acceptance': `---
|
|
184
|
-
name: bug-acceptance
|
|
180
|
+
'openfeel-bug-acceptance': `---
|
|
181
|
+
name: openfeel-bug-acceptance
|
|
185
182
|
description: 标准化 Bug 验收流程,供测试 Agent 或代码 Agent(自测后自查)调用。
|
|
186
183
|
---
|
|
187
184
|
|
|
@@ -233,8 +230,8 @@ description: 标准化 Bug 验收流程,供测试 Agent 或代码 Agent(自
|
|
|
233
230
|
|
|
234
231
|
验收通过后,核心结论写入 \`.openfeel/bugs/{module}.md\`,并在公共日志简要记录。
|
|
235
232
|
`,
|
|
236
|
-
'check-kb': `---
|
|
237
|
-
name: check-kb
|
|
233
|
+
'openfeel-check-kb': `---
|
|
234
|
+
name: openfeel-check-kb
|
|
238
235
|
description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需求返回最相关的参考信息。精确匹配无结果时自动触发语义检索回退,避免一次加载全量内容。
|
|
239
236
|
---
|
|
240
237
|
|
|
@@ -284,7 +281,7 @@ description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需
|
|
|
284
281
|
|
|
285
282
|
### 5. 语义检索回退(自动)
|
|
286
283
|
|
|
287
|
-
当精确匹配未找到任何相关条目(无 \`[+]\` 条目或所有条目与当前任务无关)时,**本技能自行执行语义检索**,无需调用方另行加载 \`search-kb\`:
|
|
284
|
+
当精确匹配未找到任何相关条目(无 \`[+]\` 条目或所有条目与当前任务无关)时,**本技能自行执行语义检索**,无需调用方另行加载 \`openfeel-search-kb\`:
|
|
288
285
|
|
|
289
286
|
1. **索引就绪检查**:检查 \`.openfeel/tmp/vectors/index.json\` 是否存在
|
|
290
287
|
2. **索引缺失时**:输出以下提示后结束,不再继续回退:
|
|
@@ -323,7 +320,7 @@ description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需
|
|
|
323
320
|
💡 如需确保索引为最新,可运行: python scripts/build_kb_index.py --dry-run
|
|
324
321
|
\`\`\`
|
|
325
322
|
|
|
326
|
-
> 注:语义检索是 \`check-kb\` 的内置回退能力。调用方只需 \`skill("check-kb")\` 一次,无需再手动调用 \`search-kb\`。
|
|
323
|
+
> 注:语义检索是 \`openfeel-check-kb\` 的内置回退能力。调用方只需 \`skill("openfeel-check-kb")\` 一次,无需再手动调用 \`openfeel-search-kb\`。
|
|
327
324
|
|
|
328
325
|
### 6. 强制检索标记
|
|
329
326
|
|
|
@@ -335,8 +332,8 @@ Skill 加载后,根据步骤 3 的匹配结果决定是否追加提示:
|
|
|
335
332
|
|
|
336
333
|
- **若步骤 3 无相关条目**:不追加强制提示,静默返回(已在步骤 4 输出"知识库中暂无相关记录,可继续执行")。
|
|
337
334
|
`,
|
|
338
|
-
'get-bugs': `---
|
|
339
|
-
name: get-bugs
|
|
335
|
+
'openfeel-get-bugs': `---
|
|
336
|
+
name: openfeel-get-bugs
|
|
340
337
|
description: 获取当前模块下状态为 open 或 fixing 的 Bug 列表,供代码 Agent 会话启动或承接时使用。
|
|
341
338
|
---
|
|
342
339
|
|
|
@@ -372,9 +369,9 @@ description: 获取当前模块下状态为 open 或 fixing 的 Bug 列表,供
|
|
|
372
369
|
|
|
373
370
|
输出:\`模块 [模块名] 当前无待处理 Bug。\`
|
|
374
371
|
`,
|
|
375
|
-
'get-stage-status': `---
|
|
376
|
-
name: get-stage-status
|
|
377
|
-
description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子计划状态、责任 Agent、是否允许自动推进以及下一步建议。用于
|
|
372
|
+
'openfeel-get-stage-status': `---
|
|
373
|
+
name: openfeel-get-stage-status
|
|
374
|
+
description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子计划状态、责任 Agent、是否允许自动推进以及下一步建议。用于 openfeel-reviewer/openfeel-executor/openfeel-feel-tester 在处理阶段任务前确认流程状态。
|
|
378
375
|
---
|
|
379
376
|
|
|
380
377
|
# 获取子计划状态
|
|
@@ -479,29 +476,29 @@ description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子
|
|
|
479
476
|
{若依赖就绪且可自动推进,列出同批次可并行启动的其他阶段}
|
|
480
477
|
|
|
481
478
|
## 下一步建议
|
|
482
|
-
{根据状态给出下一步,例如:启动 Code、等待用户、启动
|
|
479
|
+
{根据状态给出下一步,例如:启动 Code、等待用户、启动 openfeel-feel-tester、停止流程;若存在并行候选则建议批量启动}
|
|
483
480
|
\`\`\`
|
|
484
481
|
|
|
485
482
|
## 状态到下一步映射
|
|
486
483
|
|
|
487
484
|
| 状态 | 下一步建议 |
|
|
488
485
|
|------|------------|
|
|
489
|
-
| \`planned\` | 等待用户确认或
|
|
490
|
-
| \`ready_for_code\` |
|
|
491
|
-
| \`coding\` |
|
|
492
|
-
| \`ready_for_review\` |
|
|
493
|
-
| \`review_failed\` |
|
|
486
|
+
| \`planned\` | 等待用户确认或 openfeel-planner 细化计划 |
|
|
487
|
+
| \`ready_for_code\` | openfeel-planner 可启动 openfeel-executor |
|
|
488
|
+
| \`coding\` | openfeel-executor 正在开发 |
|
|
489
|
+
| \`ready_for_review\` | openfeel-executor 可启动 openfeel-reviewer 审查,或等待用户触发 |
|
|
490
|
+
| \`review_failed\` | openfeel-reviewer 可启动 openfeel-executor 修复审查问题 |
|
|
494
491
|
| \`review_passed\` | Feel 可推进到 ready_for_test |
|
|
495
|
-
| \`ready_for_test\` | Feel 可启动
|
|
496
|
-
| \`test_writing\` |
|
|
497
|
-
| \`testing\` |
|
|
498
|
-
| \`bug_found\` |
|
|
499
|
-
| \`bug_fixing\` |
|
|
492
|
+
| \`ready_for_test\` | Feel 可启动 openfeel-feel-tester |
|
|
493
|
+
| \`test_writing\` | openfeel-feel-tester 正在写测试 |
|
|
494
|
+
| \`testing\` | openfeel-feel-tester 正在测试 |
|
|
495
|
+
| \`bug_found\` | openfeel-feel-tester 可启动 openfeel-executor 修复 Bug |
|
|
496
|
+
| \`bug_fixing\` | openfeel-executor 正在修复 Bug |
|
|
500
497
|
| \`done\` | 流程完成,停止 |
|
|
501
498
|
| \`paused\` | 等待用户处理暂停原因 |
|
|
502
499
|
`,
|
|
503
|
-
'health': `---
|
|
504
|
-
name: health
|
|
500
|
+
'openfeel-health': `---
|
|
501
|
+
name: openfeel-health
|
|
505
502
|
description: 加载流水线健康检查结果,供 Agent 判断 flow.json 与工作区状态是否一致。
|
|
506
503
|
---
|
|
507
504
|
|
|
@@ -521,8 +518,8 @@ description: 加载流水线健康检查结果,供 Agent 判断 flow.json 与
|
|
|
521
518
|
|
|
522
519
|
健康检查摘要:通过项数、失败项列表及原因,失败时给出修复建议
|
|
523
520
|
`,
|
|
524
|
-
'model-check': `---
|
|
525
|
-
name: model-check
|
|
521
|
+
'openfeel-model-check': `---
|
|
522
|
+
name: openfeel-model-check
|
|
526
523
|
description: Feel 自检时检查所有 Agent 的模型配置状态,识别期望模型 vs 实际模型的差距,引导用户在目标工具中完成配置。首次配置后存储为部署模板,新项目可直接复用。
|
|
527
524
|
---
|
|
528
525
|
|
|
@@ -567,14 +564,14 @@ Feel Agent 在以下时机加载本 Skill:
|
|
|
567
564
|
| Agent 文件 | 默认模型角色 |
|
|
568
565
|
|------------|-------------|
|
|
569
566
|
| \`feel.md\` | \`primary_reasoning\`(主力推理) |
|
|
570
|
-
| \`planner.md\` | \`reasoning\`(推理) |
|
|
571
|
-
| \`executor.md\` | \`fast\`(快速) |
|
|
572
|
-
| \`reviewer.md\` | \`cross_model\`(异种推理) |
|
|
573
|
-
| \`archiver.md\` | \`reasoning\`(推理) |
|
|
574
|
-
| \`schemer.md\` | \`reasoning\`(推理) |
|
|
575
|
-
| \`feel-tester.md\` | \`reasoning\`(推理) |
|
|
576
|
-
| \`utility.md\` | \`fast\`(快速) |
|
|
577
|
-
| \`vision.md\` | \`multimodal\`(多模态) |
|
|
567
|
+
| \`openfeel-planner.md\` | \`reasoning\`(推理) |
|
|
568
|
+
| \`openfeel-executor.md\` | \`fast\`(快速) |
|
|
569
|
+
| \`openfeel-reviewer.md\` | \`cross_model\`(异种推理) |
|
|
570
|
+
| \`openfeel-archiver.md\` | \`reasoning\`(推理) |
|
|
571
|
+
| \`openfeel-schemer.md\` | \`reasoning\`(推理) |
|
|
572
|
+
| \`openfeel-feel-tester.md\` | \`reasoning\`(推理) |
|
|
573
|
+
| \`openfeel-utility.md\` | \`fast\`(快速) |
|
|
574
|
+
| \`openfeel-vision.md\` | \`multimodal\`(多模态) |
|
|
578
575
|
|
|
579
576
|
### 3. 检查 config.yaml 模型配置
|
|
580
577
|
|
|
@@ -589,14 +586,14 @@ models:
|
|
|
589
586
|
base_url: https://api.deepseek.com
|
|
590
587
|
api_key_env: DEEPSEEK_API_KEY
|
|
591
588
|
agents: # Agent 级覆盖(可选)
|
|
592
|
-
reviewer:
|
|
589
|
+
openfeel-reviewer:
|
|
593
590
|
provider: anthropic
|
|
594
591
|
model_name: claude-sonnet-4-20250514
|
|
595
592
|
base_url: https://api.anthropic.com
|
|
596
593
|
api_key_env: ANTHROPIC_API_KEY
|
|
597
|
-
executor:
|
|
594
|
+
openfeel-executor:
|
|
598
595
|
provider: deepseek
|
|
599
|
-
model_name: deepseek-
|
|
596
|
+
model_name: deepseek-flash
|
|
600
597
|
roles: # 角色级覆盖(可选)
|
|
601
598
|
cross_model:
|
|
602
599
|
provider: openai
|
|
@@ -629,8 +626,8 @@ models:
|
|
|
629
626
|
| Agent | 角色要求 | 实际模型 | 配置来源 | 状态 |
|
|
630
627
|
|-------|----------|----------|----------|:--:|
|
|
631
628
|
| Feel | 主力推理 | deepseek-v4-pro | default | ⚠️ |
|
|
632
|
-
|
|
|
633
|
-
|
|
|
629
|
+
| openfeel-reviewer | **异种推理** | deepseek-v4-pro | default | ❌ 与主力相同! |
|
|
630
|
+
| openfeel-executor | 快速 | deepseek-v4-pro | default | ⚠️ 未使用快速模型 |
|
|
634
631
|
\`\`\`
|
|
635
632
|
|
|
636
633
|
### 5. 输出检查报告
|
|
@@ -673,8 +670,8 @@ models:
|
|
|
673
670
|
| 触发条件 | 引导内容 |
|
|
674
671
|
|----------|----------|
|
|
675
672
|
| \`models\` 节不存在 | "未检测到模型配置。你需要为不同角色分配模型吗?" → 引导创建 |
|
|
676
|
-
|
|
|
677
|
-
|
|
|
673
|
+
| openfeel-reviewer 使用与主力相同模型 | "⚠️ openfeel-reviewer 当前与 Feel 使用相同模型,异种交叉审查的核心优势无法发挥。建议为 openfeel-reviewer 配置不同的模型系列。" |
|
|
674
|
+
| openfeel-executor 使用推理模型 | "⚠️ openfeel-executor 建议使用快速模型以节省成本。是否配置?" |
|
|
678
675
|
| 关键 Agent 无任何配置 | "以下 Agent 无模型配置:{列表}。请配置。" |
|
|
679
676
|
|
|
680
677
|
### 7. 写入配置
|
|
@@ -701,7 +698,7 @@ models:
|
|
|
701
698
|
model_name: xxx
|
|
702
699
|
...
|
|
703
700
|
agents:
|
|
704
|
-
reviewer:
|
|
701
|
+
openfeel-reviewer:
|
|
705
702
|
provider: xxx
|
|
706
703
|
...
|
|
707
704
|
roles:
|
|
@@ -718,12 +715,12 @@ models:
|
|
|
718
715
|
- 报告语言:中文
|
|
719
716
|
- 每次检查后将结果摘要写入 \`.openfeel/log/\`(仅首次发现关键问题时)
|
|
720
717
|
`,
|
|
721
|
-
'model-config': `---
|
|
722
|
-
name: model-config
|
|
723
|
-
description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或需要调整/新增 Agent 模型时使用。覆盖 opencode.jsonc 配置、模型名查找方法、多模态模型(
|
|
718
|
+
'openfeel-model-config': `---
|
|
719
|
+
name: openfeel-model-config
|
|
720
|
+
description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或需要调整/新增 Agent 模型时使用。覆盖 opencode.jsonc 配置、模型名查找方法、多模态模型(openfeel-vision)特殊注意事项。
|
|
724
721
|
---
|
|
725
722
|
|
|
726
|
-
# Skill: model-config
|
|
723
|
+
# Skill: openfeel-model-config
|
|
727
724
|
|
|
728
725
|
# Agent 模型查找与配置
|
|
729
726
|
|
|
@@ -732,7 +729,7 @@ description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或
|
|
|
732
729
|
- Agent 调用时报 \`Model not found: xxx\`
|
|
733
730
|
- 需要为 Agent 更换或指定模型
|
|
734
731
|
- 新增 Agent 后需要配置其模型
|
|
735
|
-
-
|
|
732
|
+
- openfeel-vision / 多模态模型无法正常调用
|
|
736
733
|
|
|
737
734
|
## 配置位置
|
|
738
735
|
|
|
@@ -741,8 +738,8 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
741
738
|
\`\`\`jsonc
|
|
742
739
|
{
|
|
743
740
|
"agent": {
|
|
744
|
-
"vision": {
|
|
745
|
-
"model": "
|
|
741
|
+
"openfeel-vision": {
|
|
742
|
+
"model": "deepseek/deepseek-flash" // 模型名格式:provider/model-id 或 model-id
|
|
746
743
|
}
|
|
747
744
|
}
|
|
748
745
|
}
|
|
@@ -750,6 +747,8 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
750
747
|
|
|
751
748
|
> ⚠️ **配置修改后必须重启 opencode 才能生效**。运行中的会话使用启动时加载的配置。
|
|
752
749
|
|
|
750
|
+
> 💡 **推荐**:可用 \`openfeel model set/get/list --scope default|global|project\` 一键读写三层级模型(工具默认 / 全局 / 当前项目),无需手工定位文件。
|
|
751
|
+
|
|
753
752
|
## 查找可用模型名
|
|
754
753
|
|
|
755
754
|
当收到 \`"Model not found: xxx. Did you mean: aaa, bbb?"\` 错误时:
|
|
@@ -759,17 +758,21 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
759
758
|
|
|
760
759
|
## Agent 定义文件中的 model 字段
|
|
761
760
|
|
|
762
|
-
\`.opencode/agents/<name>.md\`
|
|
761
|
+
\`.opencode/agents/<name>.md\` 的 frontmatter \`model:\` 字段**是生效的,且优先于** \`opencode.jsonc\` 的 \`agent.<name>.model\`(stage-40 REV-1606 实测)。opencode 模型解析优先级链:
|
|
763
762
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
763
|
+
\`\`\`
|
|
764
|
+
项目 agents frontmatter > 全局 agents frontmatter > 项目 opencode.jsonc agent.model > 全局 opencode.jsonc agent.model > opencode 默认
|
|
765
|
+
\`\`\`
|
|
767
766
|
|
|
768
|
-
|
|
767
|
+
因此修改模型时以 frontmatter 为准(只改 jsonc 会被 frontmatter 遮蔽),建议两处同步保持一致:
|
|
768
|
+
1. 修改 \`.opencode/agents/<name>.md\` 的 frontmatter \`model:\`(模板源 \`src/core/templates-data/opencode/agents/\` 同步)
|
|
769
|
+
2. (可选,保持一致)同步修改 \`opencode.jsonc\` 的 \`agent.<name>.model\`
|
|
770
|
+
|
|
771
|
+
## 多模态(openfeel-vision)模型特殊规则
|
|
769
772
|
|
|
770
773
|
- Feel 的主力模型(DeepSeek V4 Pro)不支持图片输入
|
|
771
|
-
- 遇到图片输入时 Feel 会自动委托
|
|
772
|
-
-
|
|
774
|
+
- 遇到图片输入时 Feel 会自动委托 openfeel-vision Agent
|
|
775
|
+
- openfeel-vision Agent 需要配置多模态模型(现用 \`deepseek-flash\`,备选 \`qwen3-vl-plus\`)
|
|
773
776
|
- 模型名不要随意添加前缀(如 \`alibaba/\`),以平台提示的可用名为准
|
|
774
777
|
- **模型引用格式**:\`{auth.json中的key}/{模型ID}\`,不是 \`provider.name\` 也不是 \`provider.id\`
|
|
775
778
|
- 读取 \`~/.local/share/opencode/auth.json\` 确认实际 provider key(常见:\`alibaba-cn\`、\`deepseek\`、\`zhipuai\`)
|
|
@@ -780,17 +783,17 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
780
783
|
| Agent | 模型类型 | 备注 |
|
|
781
784
|
|-------|---------|------|
|
|
782
785
|
| Feel(总统领) | 推理模型 | DeepSeek V4 Pro — 不支持多模态 |
|
|
783
|
-
|
|
|
784
|
-
|
|
|
785
|
-
|
|
|
786
|
-
|
|
|
787
|
-
|
|
|
786
|
+
| openfeel-planner | 推理模型 | — |
|
|
787
|
+
| openfeel-schemer | 推理模型 | — |
|
|
788
|
+
| openfeel-executor | 快速模型 (Flash) | — |
|
|
789
|
+
| openfeel-reviewer | 异种推理模型 (GLM) | — |
|
|
790
|
+
| openfeel-feel-tester | 推理模型 | — |
|
|
788
791
|
| 事务官 | 快速模型 (Flash) | — |
|
|
789
|
-
|
|
|
790
|
-
|
|
|
792
|
+
| openfeel-vision | 多模态模型 | 需配 deepseek-flash |
|
|
793
|
+
| openfeel-archiver | 推理模型 | — |
|
|
791
794
|
`,
|
|
792
|
-
'recover': `---
|
|
793
|
-
name: recover
|
|
795
|
+
'openfeel-recover': `---
|
|
796
|
+
name: openfeel-recover
|
|
794
797
|
description: 跨会话上下文恢复,供 Agent 在会话启动时重建流水线状态。
|
|
795
798
|
---
|
|
796
799
|
|
|
@@ -810,8 +813,8 @@ description: 跨会话上下文恢复,供 Agent 在会话启动时重建流水
|
|
|
810
813
|
|
|
811
814
|
恢复摘要:流水线状态 + 阻塞项 + 待处理任务列表
|
|
812
815
|
`,
|
|
813
|
-
'roadmap': `---
|
|
814
|
-
name: roadmap
|
|
816
|
+
'openfeel-roadmap': `---
|
|
817
|
+
name: openfeel-roadmap
|
|
815
818
|
description: 加载项目路线图,供 Agent 查看版本规划和里程碑。
|
|
816
819
|
---
|
|
817
820
|
|
|
@@ -831,8 +834,8 @@ description: 加载项目路线图,供 Agent 查看版本规划和里程碑。
|
|
|
831
834
|
|
|
832
835
|
格式化路线图摘要:版本清单 + 各版本阶段进度
|
|
833
836
|
`,
|
|
834
|
-
'search-kb': `---
|
|
835
|
-
name: search-kb
|
|
837
|
+
'openfeel-search-kb': `---
|
|
838
|
+
name: openfeel-search-kb
|
|
836
839
|
description: 语义检索 .openfeel/kb/ 项目知识库。当精确匹配无结果或任务描述模糊时,通过向量相似度搜索语义相关的知识条目。支持图谱遍历返回关联条目。
|
|
837
840
|
---
|
|
838
841
|
|
|
@@ -951,16 +954,16 @@ description: 语义检索 .openfeel/kb/ 项目知识库。当精确匹配无结
|
|
|
951
954
|
|
|
952
955
|
- 向量索引是缓存层,文件系统始终是 single source of truth。若检索结果与预期不符,检查索引是否过期(运行 \`--dry-run\` 查看变更文件)
|
|
953
956
|
- 图谱链接亦是缓存层,Markdown 文件中 \`[[wikilink]]\` 是真实数据源;图谱可随时通过 \`python scripts/build_kb_index.py --graph\` 重建
|
|
954
|
-
- 语义检索适合模糊查询和探索性搜索,精确关键词匹配优先使用 \`check-kb\`
|
|
955
|
-
- 此技能是 \`check-kb\` 的回退方案——当 \`check-kb\` 精确匹配无结果时可自动调用
|
|
957
|
+
- 语义检索适合模糊查询和探索性搜索,精确关键词匹配优先使用 \`openfeel-check-kb\`
|
|
958
|
+
- 此技能是 \`openfeel-check-kb\` 的回退方案——当 \`openfeel-check-kb\` 精确匹配无结果时可自动调用
|
|
956
959
|
- 图遍历返回的关联条目仅基于已建立的 wikilink 链接,若条目未引用或被引用其他条目,图谱中不会出现对应关联
|
|
957
960
|
`,
|
|
958
|
-
'sync-status': `---
|
|
959
|
-
name: sync-status
|
|
961
|
+
'openfeel-sync-status': `---
|
|
962
|
+
name: openfeel-sync-status
|
|
960
963
|
description: 聚合所有成员的任务进度视图,供任意 Agent 快速了解项目整体协作状态。
|
|
961
964
|
---
|
|
962
965
|
|
|
963
|
-
# Skill: sync-status
|
|
966
|
+
# Skill: openfeel-sync-status
|
|
964
967
|
|
|
965
968
|
# 聚合任务进度
|
|
966
969
|
|
|
@@ -1023,10 +1026,43 @@ description: 聚合所有成员的任务进度视图,供任意 Agent 快速了
|
|
|
1023
1026
|
|
|
1024
1027
|
格式化后的 Markdown 进度摘要,不含文件修改。
|
|
1025
1028
|
|
|
1026
|
-
Base directory for this skill: file:///C:/Code/AI/AI_Prompt/.kilo/skills/sync-status
|
|
1027
1029
|
`,
|
|
1028
|
-
'
|
|
1029
|
-
name:
|
|
1030
|
+
'openfeel-tool-usage': `---
|
|
1031
|
+
name: openfeel-tool-usage
|
|
1032
|
+
description: Agent 工具使用规范(todowrite/question/task/skill 四工具触发条件、使用要求、禁止行为 + 优先级表)。
|
|
1033
|
+
---
|
|
1034
|
+
|
|
1035
|
+
# Agent 工具使用规范
|
|
1036
|
+
|
|
1037
|
+
## 输入
|
|
1038
|
+
|
|
1039
|
+
无
|
|
1040
|
+
|
|
1041
|
+
## 执行步骤
|
|
1042
|
+
|
|
1043
|
+
### 1. todowrite — 任务列表管理
|
|
1044
|
+
触发条件(任一即用):≥3 独立步骤 / 多任务下达 / 跨文件修改。使用要求:执行前创建、单条 in_progress、完成即标 completed、新步骤追加末尾。
|
|
1045
|
+
|
|
1046
|
+
### 2. question — 向用户提问
|
|
1047
|
+
触发条件(任一必问):需求歧义 / 技术方案 ≥2 同等合理 / 不可逆后果 / 架构决策。使用要求:(Recommended) 标记、选项附后果、≤3 选项、高风险含"取消"。禁止:模糊时自行假设、多方案不选直接实施。
|
|
1048
|
+
|
|
1049
|
+
### 3. task — 子 Agent 调度
|
|
1050
|
+
触发条件:并行探索多代码区 / 复杂多步委托 general / 下游 Agent(经 Feel)。使用要求:并行一条消息多 task、prompt 含任务描述+期望返回、明确只读/可写。
|
|
1051
|
+
|
|
1052
|
+
### 4. skill — 技能加载
|
|
1053
|
+
触发条件:查阶段状态→openfeel-get-stage-status / 查知识库→openfeel-check-kb / 取 Bug→openfeel-get-bugs。使用要求:会话开始载 check-kb、处理阶段任务前载 get-stage-status、不凭记忆跳过。
|
|
1054
|
+
|
|
1055
|
+
### 5. 工具使用优先级
|
|
1056
|
+
| 场景 | 优先工具 | 禁止做法 |
|
|
1057
|
+
|------|---------|----------|
|
|
1058
|
+
| 多步骤任务 | todowrite | 凭记忆逐条执行 |
|
|
1059
|
+
| 需求不明确 | question | 自行假设后动手 |
|
|
1060
|
+
| 探索代码 | task(explore) | 手动逐个 grep/read |
|
|
1061
|
+
| 获取状态 | skill(openfeel-get-stage-status) | 凭记忆推断 |
|
|
1062
|
+
| 批量文件操作 | task(general) | 串行逐个处理 |
|
|
1063
|
+
`,
|
|
1064
|
+
'openfeel-update-stage-status': `---
|
|
1065
|
+
name: openfeel-update-stage-status
|
|
1030
1066
|
description: 标准化更新 .openfeel/plan/{series}/{stage}/status.md 的子计划状态、责任 Agent 和状态记录,避免各 Agent 随意改写状态文件。适用于自动闭环和人工流程中的阶段状态变更。
|
|
1031
1067
|
---
|
|
1032
1068
|
|
|
@@ -1124,10 +1160,10 @@ planned | ready_for_code | coding | ready_for_review | review_failed | review_pa
|
|
|
1124
1160
|
|
|
1125
1161
|
### 6. 合并状态处理
|
|
1126
1162
|
|
|
1127
|
-
当子计划状态变为 \`done\` 或 \`review_passed\`(且
|
|
1163
|
+
当子计划状态变为 \`done\` 或 \`review_passed\`(且 openfeel-reviewer 验收完毕)时,根据 \`.openfeel/config.yaml\` 中的 \`merge_mode\` 决定合并行为:
|
|
1128
1164
|
|
|
1129
1165
|
- **\`merge_mode=auto\`**:
|
|
1130
|
-
1. 将 \`合并状态\` 更新为 \`merged\`(实际合并由
|
|
1166
|
+
1. 将 \`合并状态\` 更新为 \`merged\`(实际合并由 openfeel-executor 执行 git 操作,Skill 仅更新状态字段)
|
|
1131
1167
|
2. 将 \`合并状态\` 更新为 \`cleaned\`
|
|
1132
1168
|
3. 在状态记录中注明"自动合并"
|
|
1133
1169
|
- **\`merge_mode=manual\`**:
|
|
@@ -1145,8 +1181,8 @@ planned | ready_for_code | coding | ready_for_review | review_failed | review_pa
|
|
|
1145
1181
|
- 自动推进:保持 {enabled/disabled}
|
|
1146
1182
|
\`\`\`
|
|
1147
1183
|
`,
|
|
1148
|
-
'wizard': `---
|
|
1149
|
-
name: wizard
|
|
1184
|
+
'openfeel-wizard': `---
|
|
1185
|
+
name: openfeel-wizard
|
|
1150
1186
|
description: 交互式流水线向导,供 Agent 在终端中逐步推进流水线阶段。
|
|
1151
1187
|
---
|
|
1152
1188
|
|
|
@@ -1167,304 +1203,133 @@ description: 交互式流水线向导,供 Agent 在终端中逐步推进流水
|
|
|
1167
1203
|
向导推进结果:阶段 phase 变化(from → to),结束/退出提示
|
|
1168
1204
|
|
|
1169
1205
|
> 注:需交互式终端(TTY),非交互环境请改用 \`openfeel flow advance --stage <id> --to <phase>\`
|
|
1206
|
+
`,
|
|
1207
|
+
'openfeel-workspace': `---
|
|
1208
|
+
name: openfeel-workspace
|
|
1209
|
+
description: 会话启动时检查并补齐 .openfeel/ 工作区目录结构与空文件的标准化操作步骤(mkdir 哪些目录、创建哪些空文件、读 .info.json 取用户名)。
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
# 工作区启动自检
|
|
1213
|
+
|
|
1214
|
+
## 输入
|
|
1215
|
+
|
|
1216
|
+
无(自动按 .openfeel/.info.json 与 ~/.openfeel/update_infos.md 推断)
|
|
1217
|
+
|
|
1218
|
+
## 执行步骤
|
|
1219
|
+
|
|
1220
|
+
### 1. 读取用户名
|
|
1221
|
+
读 \`.openfeel/.info.json\` 的 \`user\` 字段;缺失则 \`git config user.name\`。
|
|
1222
|
+
|
|
1223
|
+
### 2. 检查公共域目录(缺失则 mkdir -p)
|
|
1224
|
+
\`.openfeel/dev/note/\`、\`.openfeel/log/\`、\`.openfeel/code_review/\`、\`.openfeel/bugs/\`、\`.openfeel/plan/\`、\`.openfeel/kb/\`、\`.openfeel/tmp/\`
|
|
1225
|
+
|
|
1226
|
+
### 3. 检查公共域文件(缺失则创建空文件)
|
|
1227
|
+
\`.openfeel/dev/dev_core.md\`、\`.openfeel/dev/current.md\`、\`.openfeel/dev/decisions.md\`、\`.openfeel/kb/index.md\`
|
|
1228
|
+
|
|
1229
|
+
### 4. 检查私域目录(基于 {username})
|
|
1230
|
+
\`.openfeel/users/{username}/log/\`、\`note/\`、\`code_review/\`、\`bugs/\`、\`tmp/\`
|
|
1231
|
+
|
|
1232
|
+
### 5. 检查私域文件
|
|
1233
|
+
\`.openfeel/users/{username}/dev_last.md\`
|
|
1234
|
+
|
|
1235
|
+
### 6. 增量更新复核
|
|
1236
|
+
检查 \`~/.openfeel/update_infos.md\`,若存在未修复条目(追加/异常),提醒用户重启会话或委托 Feel 处理;本 Agent 不自行修改该文件。
|
|
1237
|
+
|
|
1238
|
+
> 目录结构语义、公共/私域分区、用户身份约束、路径自校验规则见全局 AGENTS.md(约束);本 skill 仅承载操作步骤。
|
|
1170
1239
|
`,
|
|
1171
1240
|
};
|
|
1172
1241
|
// AUTO-GENERATED-END: SKILL_DEFINITIONS
|
|
1173
|
-
// ─── 新增的 Skill 名称列表 ─────────────────────────────────────────
|
|
1174
|
-
const NEW_SKILL_NAMES = [
|
|
1175
|
-
'bug-acceptance',
|
|
1176
|
-
'check-kb',
|
|
1177
|
-
'get-bugs',
|
|
1178
|
-
'get-stage-status',
|
|
1179
|
-
'model-check',
|
|
1180
|
-
'search-kb',
|
|
1181
|
-
'sync-status',
|
|
1182
|
-
'update-stage-status',
|
|
1183
|
-
];
|
|
1184
1242
|
// ─── 辅助函数 ──────────────────────────────────────────────────────
|
|
1243
|
+
/** 写全文时的受管内容组合:frontmatter 序列化 + 正文标记包裹 */
|
|
1244
|
+
function composeManagedContent(content, type) {
|
|
1245
|
+
const split = type === 'markdown' ? splitFrontmatter(content) : null;
|
|
1246
|
+
if (split) {
|
|
1247
|
+
return serializeFrontmatter(split.frontmatter) + wrapRegion(split.body, type);
|
|
1248
|
+
}
|
|
1249
|
+
return wrapRegion(content, type);
|
|
1250
|
+
}
|
|
1185
1251
|
/**
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
* - 文件不存在 → created
|
|
1190
|
-
* - 文件已存在 + hash 匹配 update_state 中记录 → 安全覆盖 → updated
|
|
1191
|
-
* - 文件已存在 + hash 不匹配 → 拒绝覆盖 → conflicts
|
|
1192
|
-
* - 文件已存在 + 不在 update_state 管理中 → 安全覆盖 → updated(降级)
|
|
1193
|
-
* - 文件已存在 + update_state 不存在或损坏 → 安全覆盖 → updated(降级为旧行为)
|
|
1194
|
-
*
|
|
1195
|
-
* REV-001:此函数仅负责分类(created/updated/skipped/conflicts),
|
|
1196
|
-
* hash 的实际更新在 updateProject() 末尾统一处理,
|
|
1197
|
-
* 确保"冲突路径非冲突文件 hash 同步更新"。
|
|
1252
|
+
* 受管文件写入(控制区优先 + hash 兜底 三态)。
|
|
1253
|
+
* 分派依据 detectFileType;jsonc 不走本函数(调用方走深度合并)。
|
|
1254
|
+
* target 供 update_infos 路径记录:全局资产 { isGlobal: true },项目资产 { isGlobal: false, projectRoot }。
|
|
1198
1255
|
*/
|
|
1199
|
-
function
|
|
1256
|
+
function writeManagedFile(filePath, content, stateKey, updateState, target) {
|
|
1257
|
+
const type = detectFileType(filePath);
|
|
1258
|
+
if (type === null || type === 'jsonc') {
|
|
1259
|
+
// 无策略 / JSONC:不应到达(jsonc 由 updateProject 单独走深度合并)
|
|
1260
|
+
throw new Error(`writeManagedFile: 不支持的类型 ${filePath}`);
|
|
1261
|
+
}
|
|
1262
|
+
// 拆 incoming 的 frontmatter + 正文(gitignore 无 frontmatter,body=content)
|
|
1263
|
+
const incomingSplit = type === 'markdown' ? splitFrontmatter(content) : null;
|
|
1264
|
+
const incomingFm = incomingSplit?.frontmatter ?? null;
|
|
1265
|
+
const incomingBody = incomingSplit?.body ?? content;
|
|
1266
|
+
// 不存在 → created(写全文:frontmatter + 标记包裹正文)
|
|
1200
1267
|
if (!existsSync(filePath)) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
1204
|
-
created.push(relativePath);
|
|
1205
|
-
return;
|
|
1268
|
+
atomicWriteFileSync(filePath, composeManagedContent(content, type));
|
|
1269
|
+
return 'created';
|
|
1206
1270
|
}
|
|
1207
1271
|
const existing = readFileSync(filePath, 'utf-8');
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1220
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
1221
|
-
updated.push(relativePath);
|
|
1222
|
-
return;
|
|
1272
|
+
const existingSplit = type === 'markdown' ? splitFrontmatter(existing) : null;
|
|
1273
|
+
const existingFm = existingSplit?.frontmatter ?? null;
|
|
1274
|
+
const existingBody = existingSplit?.body ?? existing;
|
|
1275
|
+
const region = parseRegion(existingBody, type);
|
|
1276
|
+
if (region.status === 'ok') {
|
|
1277
|
+
// 含标记:frontmatter 合并 + 正文区内替换,全文比对(REV-901 skip 判定)
|
|
1278
|
+
// incoming 无 frontmatter(如 core.md/AGENTS.md 模板)时保留用户已有 frontmatter,避免写入丢失(REV-1101)
|
|
1279
|
+
const newFm = existingFm && incomingFm ? mergeFrontmatter(existingFm, incomingFm) : (existingFm ?? incomingFm);
|
|
1280
|
+
const newBody = replaceRegion(existingBody, incomingBody, type);
|
|
1281
|
+
const newContent = newFm ? serializeFrontmatter(newFm) + newBody : newBody;
|
|
1282
|
+
if (newContent === normalize(existing)) {
|
|
1283
|
+
return 'skipped';
|
|
1223
1284
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
return;
|
|
1285
|
+
atomicWriteFileSync(filePath, newContent);
|
|
1286
|
+
return 'updated';
|
|
1227
1287
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
*/
|
|
1236
|
-
function parseJsonc(text) {
|
|
1237
|
-
// 去除 \r 控制字符(避免 Windows CRLF 导致 JSON 解析报 "bad control character")
|
|
1238
|
-
const normalized = text.replace(/\r/g, '');
|
|
1239
|
-
// 逐字符处理,去除行注释(跳过字符串内部,避免匹配到 https:// 等 URL 中的 //)
|
|
1240
|
-
let result = '';
|
|
1241
|
-
let inString = false;
|
|
1242
|
-
let inEscape = false;
|
|
1243
|
-
let i = 0;
|
|
1244
|
-
while (i < normalized.length) {
|
|
1245
|
-
const ch = normalized[i];
|
|
1246
|
-
const next = normalized[i + 1];
|
|
1247
|
-
// 处理转义字符
|
|
1248
|
-
if (inEscape) {
|
|
1249
|
-
result += ch;
|
|
1250
|
-
inEscape = false;
|
|
1251
|
-
i++;
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
if (ch === '\\' && inString) {
|
|
1255
|
-
result += ch;
|
|
1256
|
-
inEscape = true;
|
|
1257
|
-
i++;
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
// 字符串边界
|
|
1261
|
-
if (ch === '"') {
|
|
1262
|
-
inString = !inString;
|
|
1263
|
-
result += ch;
|
|
1264
|
-
i++;
|
|
1265
|
-
continue;
|
|
1266
|
-
}
|
|
1267
|
-
// 行注释:不在字符串内时遇到 //
|
|
1268
|
-
if (!inString && ch === '/' && next === '/') {
|
|
1269
|
-
// 跳过直到行尾
|
|
1270
|
-
i += 2; // 跳过 //
|
|
1271
|
-
while (i < normalized.length && normalized[i] !== '\n') {
|
|
1272
|
-
i++;
|
|
1273
|
-
}
|
|
1274
|
-
// 保留换行符
|
|
1275
|
-
if (i < normalized.length && normalized[i] === '\n') {
|
|
1276
|
-
result += '\n';
|
|
1277
|
-
i++;
|
|
1278
|
-
}
|
|
1279
|
-
continue;
|
|
1288
|
+
if (region.status === 'none') {
|
|
1289
|
+
// 无标记:hash 兜底(N3)
|
|
1290
|
+
const fileState = updateState?.files[stateKey];
|
|
1291
|
+
if (fileState && hashContent(existing) === fileState.hash) {
|
|
1292
|
+
// adopt:存量框架文件未改 → 直接写带标记的新框架内容(等价 created 记为 updated)
|
|
1293
|
+
atomicWriteFileSync(filePath, composeManagedContent(content, type));
|
|
1294
|
+
return 'updated';
|
|
1280
1295
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1296
|
+
// 追加受管区(标记包裹正文,不触碰 frontmatter),并记录待会话启动复核
|
|
1297
|
+
const appendedRegion = wrapRegion(incomingBody, type);
|
|
1298
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
1299
|
+
atomicWriteFileSync(filePath, `${existing}${sep}${appendedRegion}`);
|
|
1300
|
+
appendUpdateInfo('appended', target.isGlobal ? { absolutePath: filePath } : { projectRoot: target.projectRoot, relativePath: stateKey });
|
|
1301
|
+
return 'appended';
|
|
1283
1302
|
}
|
|
1284
|
-
|
|
1303
|
+
// malformed(>1 对 / 不成对):不追加、不写盘,仅记录异常条目待人工修复标记;
|
|
1304
|
+
// 修复后下次 update 走 ok(区内替换)/ none(hash 兜底)正常路径,杜绝重复追加死循环(REV-1001 修订)
|
|
1305
|
+
appendUpdateInfo('anomaly', target.isGlobal ? { absolutePath: filePath } : { projectRoot: target.projectRoot, relativePath: stateKey });
|
|
1306
|
+
return 'skipped';
|
|
1285
1307
|
}
|
|
1286
1308
|
/**
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1309
|
+
* 写单个全局资产文件(REV-1205 最小侵入抽取,供 migrate 复用)。
|
|
1310
|
+
* 等价 writeManagedFile(filePath, content, filePath, state, { isGlobal: true }),
|
|
1311
|
+
* 即 stateKey = 绝对路径、target 为全局资产。
|
|
1312
|
+
* @returns 动作分类,供调用方报告。
|
|
1289
1313
|
*/
|
|
1290
|
-
function
|
|
1291
|
-
|
|
1292
|
-
let jsoncObj;
|
|
1293
|
-
let rawContent;
|
|
1294
|
-
if (existsSync(jsoncPath)) {
|
|
1295
|
-
// 已有文件 → 解析并合并,保留原始字符串用于后续补丁式更新
|
|
1296
|
-
rawContent = readFileSync(jsoncPath, 'utf-8');
|
|
1297
|
-
jsoncObj = parseJsonc(rawContent);
|
|
1298
|
-
}
|
|
1299
|
-
else {
|
|
1300
|
-
// 新文件 → 基础结构
|
|
1301
|
-
jsoncObj = {
|
|
1302
|
-
$schema: 'https://opencode.openfeel/config.json',
|
|
1303
|
-
default_agent: 'feel',
|
|
1304
|
-
instructions: ['AGENTS.md', '.opencode/instructions/core.md'],
|
|
1305
|
-
skills: {},
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
// 更新 default_agent
|
|
1309
|
-
jsoncObj.default_agent = 'feel';
|
|
1310
|
-
// 确保 instructions 存在
|
|
1311
|
-
if (!jsoncObj.instructions) {
|
|
1312
|
-
jsoncObj.instructions = ['AGENTS.md', '.opencode/instructions/core.md'];
|
|
1313
|
-
}
|
|
1314
|
-
// 合并 skills:保留原有 skill,添加新的 /opfx:* skill
|
|
1315
|
-
const skills = jsoncObj.skills || {};
|
|
1316
|
-
for (const name of NEW_SKILL_NAMES) {
|
|
1317
|
-
skills[name] = `.opencode/skills/${name}`;
|
|
1318
|
-
}
|
|
1319
|
-
jsoncObj.skills = skills;
|
|
1320
|
-
// 格式化为 JSONC 输出:若有原始内容则基于它做补丁式替换,否则从头构建
|
|
1321
|
-
return formatJsonc(jsoncObj, rawContent);
|
|
1322
|
-
}
|
|
1323
|
-
/**
|
|
1324
|
-
* 将对象格式化为美观的 JSONC 字符串
|
|
1325
|
-
* 若提供了 originalContent,基于原始字符串做补丁式替换,
|
|
1326
|
-
* 仅更新 default_agent 和 skills,其余字段(含注释和未识别字段)原样保留。
|
|
1327
|
-
*/
|
|
1328
|
-
function formatJsonc(obj, originalContent) {
|
|
1329
|
-
// 有原始内容 → 补丁模式:仅替换需要更新的字段,保留注释和未知字段
|
|
1330
|
-
if (originalContent) {
|
|
1331
|
-
let result = originalContent;
|
|
1332
|
-
// 替换 "default_agent" 的值
|
|
1333
|
-
result = result.replace(/("default_agent"\s*:\s*)("[^"]*")/, `$1"${obj.default_agent}"`);
|
|
1334
|
-
// 替换 "skills" 块
|
|
1335
|
-
const skills = obj.skills;
|
|
1336
|
-
result = replaceSkillsFieldInJsonc(result, skills);
|
|
1337
|
-
return result;
|
|
1338
|
-
}
|
|
1339
|
-
// 无原始内容 → 从头构建(新文件场景)
|
|
1340
|
-
return buildJsoncFromObject(obj);
|
|
1314
|
+
export function deployGlobalAsset(filePath, content, state) {
|
|
1315
|
+
return writeManagedFile(filePath, content, filePath, state, { isGlobal: true });
|
|
1341
1316
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
// 找到匹配的 },处理字符串内的大括号
|
|
1357
|
-
let depth = 1;
|
|
1358
|
-
let closeBraceIdx = openBraceIdx;
|
|
1359
|
-
let inString = false;
|
|
1360
|
-
let inEscape = false;
|
|
1361
|
-
for (let i = openBraceIdx + 1; i < content.length && depth > 0; i++) {
|
|
1362
|
-
const ch = content[i];
|
|
1363
|
-
if (inEscape) {
|
|
1364
|
-
inEscape = false;
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1367
|
-
if (ch === '\\' && inString) {
|
|
1368
|
-
inEscape = true;
|
|
1369
|
-
continue;
|
|
1370
|
-
}
|
|
1371
|
-
if (ch === '"') {
|
|
1372
|
-
inString = !inString;
|
|
1373
|
-
continue;
|
|
1374
|
-
}
|
|
1375
|
-
if (!inString) {
|
|
1376
|
-
if (ch === '{') {
|
|
1377
|
-
depth++;
|
|
1378
|
-
}
|
|
1379
|
-
else if (ch === '}') {
|
|
1380
|
-
depth--;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
if (depth === 0) {
|
|
1384
|
-
closeBraceIdx = i;
|
|
1317
|
+
/** 将 writeManagedFile 的动作分发到对应结果数组 */
|
|
1318
|
+
function pushAction(action, stateKey, created, updated, skipped, appended) {
|
|
1319
|
+
switch (action) {
|
|
1320
|
+
case 'created':
|
|
1321
|
+
created.push(stateKey);
|
|
1322
|
+
break;
|
|
1323
|
+
case 'updated':
|
|
1324
|
+
updated.push(stateKey);
|
|
1325
|
+
break;
|
|
1326
|
+
case 'skipped':
|
|
1327
|
+
skipped.push(stateKey);
|
|
1328
|
+
break;
|
|
1329
|
+
case 'appended':
|
|
1330
|
+
appended.push(stateKey);
|
|
1385
1331
|
break;
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
// 检查 } 后面是否有逗号:若 } 后紧跟(仅跳过空白)有逗号,则将逗号及其后空白纳入 endIdx
|
|
1389
|
-
let endIdx = closeBraceIdx + 1;
|
|
1390
|
-
let scanIdx = endIdx;
|
|
1391
|
-
while (scanIdx < content.length && /[ \t]/.test(content[scanIdx])) {
|
|
1392
|
-
scanIdx++;
|
|
1393
|
-
}
|
|
1394
|
-
if (scanIdx < content.length && content[scanIdx] === ',') {
|
|
1395
|
-
scanIdx++; // 跳过逗号
|
|
1396
|
-
// 跳过逗号后的空白(包括换行)
|
|
1397
|
-
while (scanIdx < content.length && /[\s\n\r]/.test(content[scanIdx])) {
|
|
1398
|
-
scanIdx++;
|
|
1399
|
-
}
|
|
1400
|
-
endIdx = scanIdx;
|
|
1401
|
-
}
|
|
1402
|
-
// 确定替换后是否需要末尾逗号:检查 skills 之后是否还有其他顶层字段
|
|
1403
|
-
let hasMoreFields = false;
|
|
1404
|
-
if (endIdx < content.length) {
|
|
1405
|
-
// 从 endIdx 找到内容末尾前最后一个 }(根对象闭合)
|
|
1406
|
-
const remaining = content.slice(endIdx);
|
|
1407
|
-
const rootCloseIdx = remaining.lastIndexOf('}');
|
|
1408
|
-
if (rootCloseIdx > 0) {
|
|
1409
|
-
const between = remaining.slice(0, rootCloseIdx);
|
|
1410
|
-
hasMoreFields = /[^\s]/.test(between);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
// 构建新的 skills 块内容
|
|
1414
|
-
const skillEntries = Object.entries(skills);
|
|
1415
|
-
const skillLines = skillEntries.map(([key, value], idx) => {
|
|
1416
|
-
const comma = idx < skillEntries.length - 1 ? ',' : '';
|
|
1417
|
-
return ` "${key}": "${value}"${comma}`;
|
|
1418
|
-
});
|
|
1419
|
-
const blockContent = skillLines.join('\n');
|
|
1420
|
-
const newBlock = `"skills": {\n${blockContent}\n }${hasMoreFields ? ',' : ''}`;
|
|
1421
|
-
return content.slice(0, keyStart) + newBlock + content.slice(endIdx);
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* 从对象从头构建 JSONC(无原始内容时的回退路径)
|
|
1425
|
-
*/
|
|
1426
|
-
function buildJsoncFromObject(obj) {
|
|
1427
|
-
const skills = obj.skills;
|
|
1428
|
-
// 构建 skills 条目行
|
|
1429
|
-
const skillEntries = Object.entries(skills);
|
|
1430
|
-
const skillsLines = skillEntries.map(([key, value], index) => {
|
|
1431
|
-
const comma = index < skillEntries.length - 1 ? ',' : '';
|
|
1432
|
-
return ` "${key}": "${value}"${comma}`;
|
|
1433
|
-
});
|
|
1434
|
-
const instructions = obj.instructions;
|
|
1435
|
-
const instructionLines = instructions.map((inst, index) => {
|
|
1436
|
-
const comma = index < instructions.length - 1 ? ',' : '';
|
|
1437
|
-
return ` "${inst}"${comma}`;
|
|
1438
|
-
});
|
|
1439
|
-
// 构建完整 JSONC
|
|
1440
|
-
const hasExperimental = typeof obj.experimental === 'object' && obj.experimental !== null;
|
|
1441
|
-
const lines = [
|
|
1442
|
-
'{',
|
|
1443
|
-
' "$schema": "https://opencode.openfeel/config.json",',
|
|
1444
|
-
` "default_agent": "${obj.default_agent}",`,
|
|
1445
|
-
' "instructions": [',
|
|
1446
|
-
...instructionLines,
|
|
1447
|
-
' ],',
|
|
1448
|
-
' "skills": {',
|
|
1449
|
-
...skillsLines,
|
|
1450
|
-
hasExperimental ? ' },' : ' }',
|
|
1451
|
-
];
|
|
1452
|
-
// 如果有 experimental 字段,保留它
|
|
1453
|
-
if (hasExperimental) {
|
|
1454
|
-
const exp = obj.experimental;
|
|
1455
|
-
const expEntries = Object.entries(exp);
|
|
1456
|
-
const expLines = expEntries.map(([key, value], index) => {
|
|
1457
|
-
const comma = index < expEntries.length - 1 ? ',' : '';
|
|
1458
|
-
const val = typeof value === 'string' ? `"${value}"` : String(value);
|
|
1459
|
-
return ` "${key}": ${val}${comma}`;
|
|
1460
|
-
});
|
|
1461
|
-
lines.push(' "experimental": {');
|
|
1462
|
-
lines.push(...expLines);
|
|
1463
|
-
lines.push(' }');
|
|
1464
1332
|
}
|
|
1465
|
-
lines.push('}');
|
|
1466
|
-
lines.push(''); // 末尾换行符
|
|
1467
|
-
return lines.join('\n');
|
|
1468
1333
|
}
|
|
1469
1334
|
// ─── 主函数 ────────────────────────────────────────────────────────
|
|
1470
1335
|
/**
|
|
@@ -1478,91 +1343,27 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1478
1343
|
const created = [];
|
|
1479
1344
|
const updated = [];
|
|
1480
1345
|
const skipped = [];
|
|
1481
|
-
|
|
1346
|
+
const appended = [];
|
|
1347
|
+
// ── 增量更新:加载项目 state(AGENTS.md / 项目 opencode.jsonc)与全局 state(框架资产)──
|
|
1482
1348
|
const state = loadUpdateState(projectPath);
|
|
1349
|
+
const globalState = loadGlobalUpdateState();
|
|
1483
1350
|
const conflicts = [];
|
|
1484
|
-
const agentsDir = resolve(projectPath, '.opencode', 'agents');
|
|
1485
|
-
const skillsDir = resolve(projectPath, '.opencode', 'skills');
|
|
1486
|
-
// 确保目标目录存在
|
|
1487
|
-
mkdirSync(agentsDir, { recursive: true });
|
|
1488
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
1489
1351
|
// 过滤:仅处理选中的工具
|
|
1490
1352
|
if (!selectedTools.includes('opencode')) {
|
|
1491
|
-
return { created: [], updated: [], skipped: [], conflicts: [] };
|
|
1492
|
-
}
|
|
1493
|
-
// ── AGENTS.md 语言同步逻辑 ──
|
|
1494
|
-
let agentsDirHasContent = false;
|
|
1495
|
-
try {
|
|
1496
|
-
agentsDirHasContent = existsSync(agentsDir) && readdirSync(agentsDir).length > 0;
|
|
1497
|
-
}
|
|
1498
|
-
catch {
|
|
1499
|
-
agentsDirHasContent = false;
|
|
1353
|
+
return { created: [], updated: [], skipped: [], conflicts: [], appended: [] };
|
|
1500
1354
|
}
|
|
1501
|
-
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
const deployLang = lang ?? globalConfig.lang ?? 'zh-CN';
|
|
1507
|
-
const agentsMdContent = loadTemplate(deployLang, 'agents-md');
|
|
1508
|
-
writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
|
|
1509
|
-
created.push('AGENTS.md');
|
|
1510
|
-
}
|
|
1511
|
-
else if (!agentsMdExists && options?.lang) {
|
|
1512
|
-
// 情况 2b:已有 agents 目录但 AGENTS.md 被手动删除 + --lang 指定
|
|
1513
|
-
// → 用指定语言重新创建 AGENTS.md
|
|
1514
|
-
const agentsMdContent = loadTemplate(options.lang, 'agents-md');
|
|
1515
|
-
writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
|
|
1516
|
-
created.push('AGENTS.md');
|
|
1517
|
-
}
|
|
1518
|
-
else if (agentsMdExists && options?.lang) {
|
|
1519
|
-
// 情况 2:已有项目 + --lang 参数指定了语言
|
|
1520
|
-
const projectLang = getLang(projectPath);
|
|
1521
|
-
const requestedLang = options.lang;
|
|
1522
|
-
if (requestedLang !== projectLang) {
|
|
1523
|
-
// 语言不同 → 提示确认
|
|
1524
|
-
if (options?.force) {
|
|
1525
|
-
// --force → 跳过确认,直接覆盖
|
|
1526
|
-
const agentsMdContent = loadTemplate(requestedLang, 'agents-md');
|
|
1527
|
-
writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
|
|
1528
|
-
updated.push('AGENTS.md');
|
|
1529
|
-
}
|
|
1530
|
-
else if (options?.interactive) {
|
|
1531
|
-
// 交互模式 → 输出冲突警告,跳过 AGENTS.md,继续执行后续框架更新
|
|
1532
|
-
console.warn(`[update] AGENTS.md language mismatch: project=${projectLang}, requested=${requestedLang}. Skipped. Use --force to override.`);
|
|
1533
|
-
skipped.push('AGENTS.md (language conflict)');
|
|
1534
|
-
}
|
|
1535
|
-
else {
|
|
1536
|
-
// 非交互模式 → 输出警告,不覆盖
|
|
1537
|
-
console.warn(`[update] AGENTS.md language mismatch: project=${projectLang}, requested=${requestedLang}. Skipped. Use --force to override.`);
|
|
1538
|
-
skipped.push('AGENTS.md');
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
else {
|
|
1542
|
-
// 语言相同 → 比较内容,模板更新时传播部署
|
|
1543
|
-
const templateContent = loadTemplate(requestedLang, 'agents-md');
|
|
1544
|
-
const existingContent = readFileSync(agentsMdPath, 'utf-8');
|
|
1545
|
-
if (existingContent !== templateContent) {
|
|
1546
|
-
writeFileSync(agentsMdPath, templateContent, 'utf-8');
|
|
1547
|
-
updated.push('AGENTS.md');
|
|
1548
|
-
}
|
|
1549
|
-
else {
|
|
1550
|
-
skipped.push('AGENTS.md (language unchanged)');
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
else if (agentsMdExists) {
|
|
1555
|
-
// 情况 3:已有项目 + 无 --lang 参数 → 比较内容,模板更新时传播部署
|
|
1556
|
-
const templateContent = loadTemplate(lang, 'agents-md');
|
|
1557
|
-
const existingContent = readFileSync(agentsMdPath, 'utf-8');
|
|
1558
|
-
if (existingContent !== templateContent) {
|
|
1559
|
-
writeFileSync(agentsMdPath, templateContent, 'utf-8');
|
|
1560
|
-
updated.push('AGENTS.md');
|
|
1561
|
-
}
|
|
1562
|
-
else {
|
|
1563
|
-
skipped.push('AGENTS.md (use existing)');
|
|
1564
|
-
}
|
|
1355
|
+
// N8:检测项目内遗留 .opencode/{agents,skills,instructions},只提示不迁移
|
|
1356
|
+
const legacyDirs = ['agents', 'skills', 'instructions']
|
|
1357
|
+
.filter((d) => existsSync(resolve(projectPath, '.opencode', d)));
|
|
1358
|
+
if (legacyDirs.length > 0) {
|
|
1359
|
+
console.warn(`[update] 检测到项目内旧布局 .opencode/{${legacyDirs.join(',')}},请运行 openfeel migrate(stage-39 提供)迁移。`);
|
|
1565
1360
|
}
|
|
1361
|
+
// 全局部署目标(D1)
|
|
1362
|
+
const agentsDir = getGlobalAgentsDir();
|
|
1363
|
+
const skillsDir = getGlobalSkillsDir();
|
|
1364
|
+
// 确保全局目标目录存在
|
|
1365
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
1366
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
1566
1367
|
// 记录项目语言映射到全局配置
|
|
1567
1368
|
// 使用 options?.lang(用户通过 --lang 显式指定的语言)优先,
|
|
1568
1369
|
// 回退到 lang(getCliLang 三级回退结果)。确保记录的语言与实际部署语言一致。
|
|
@@ -1572,76 +1373,131 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1572
1373
|
catch {
|
|
1573
1374
|
// 记录失败不影响主流程
|
|
1574
1375
|
}
|
|
1575
|
-
//
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1376
|
+
// 全局 state 可写实例:首次 null → 降级重建(后续全量写入全局资产,防静默不部署)
|
|
1377
|
+
const newGlobalState = globalState ?? createGlobalUpdateState({});
|
|
1378
|
+
// v1.1.1:存量全局 state 的 core.md key → 全局 AGENTS.md key(一次性重映射)
|
|
1379
|
+
// 说明:在 newGlobalState 创建后执行(newGlobalState = globalState ?? createGlobalUpdateState({});
|
|
1380
|
+
// 非 null 时二者同引用,故直接改 globalState 即改 newGlobalState)。
|
|
1381
|
+
const staleCoreKey = getGlobalCoreMdPath();
|
|
1382
|
+
if (globalState && globalState.files[staleCoreKey] && !globalState.files[getGlobalAgentsMdPath()]) {
|
|
1383
|
+
globalState.files[getGlobalAgentsMdPath()] = globalState.files[staleCoreKey];
|
|
1384
|
+
delete globalState.files[staleCoreKey];
|
|
1385
|
+
}
|
|
1386
|
+
// 0. 生成全局框架约束 AGENTS.md(~/.config/opencode/AGENTS.md)
|
|
1387
|
+
const agentsMdPath = getGlobalAgentsMdPath();
|
|
1388
|
+
const agentsMdContent = loadTemplate(lang, 'agents-md');
|
|
1389
|
+
{
|
|
1390
|
+
const action = deployGlobalAsset(agentsMdPath, agentsMdContent, newGlobalState);
|
|
1391
|
+
pushAction(action, agentsMdPath, created, updated, skipped, appended);
|
|
1392
|
+
}
|
|
1393
|
+
// 1. 生成 Agent 定义文件到全局 agents 目录(绝对路径作 state key)
|
|
1582
1394
|
for (const name of listAgentIds(lang)) {
|
|
1583
1395
|
const content = loadAgentTemplate(lang, name);
|
|
1584
1396
|
const filePath = join(agentsDir, `${name}.md`);
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1397
|
+
const action = deployGlobalAsset(filePath, content, newGlobalState);
|
|
1398
|
+
pushAction(action, filePath, created, updated, skipped, appended);
|
|
1587
1399
|
}
|
|
1588
|
-
// 2. 生成 Skill
|
|
1400
|
+
// 2. 生成 Skill 定义文件到全局 skills 目录(每个 Skill 一个子目录,包含 SKILL.md)
|
|
1589
1401
|
for (const [name, content] of Object.entries(SKILL_DEFINITIONS)) {
|
|
1590
1402
|
const skillSubDir = join(skillsDir, name);
|
|
1591
1403
|
mkdirSync(skillSubDir, { recursive: true });
|
|
1592
1404
|
const filePath = join(skillSubDir, 'SKILL.md');
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1405
|
+
const action = deployGlobalAsset(filePath, content, newGlobalState);
|
|
1406
|
+
pushAction(action, filePath, created, updated, skipped, appended);
|
|
1407
|
+
}
|
|
1408
|
+
// 3. 全局 opencode.jsonc:深度合并(保留用户字段),加全局锁 + 原子写
|
|
1409
|
+
const globalJsoncPath = getGlobalOpencodeJsoncPath();
|
|
1410
|
+
// REV-1805:read-merge-write 全部置于锁内,消除与 model-config 锁内读写的 TOCTOU 竞态
|
|
1411
|
+
const globalJsoncNew = withFileLock(globalLockPath('global-opencode-jsonc'), () => {
|
|
1412
|
+
const current = existsSync(globalJsoncPath) ? readFileSync(globalJsoncPath, 'utf-8') : '{}\n';
|
|
1413
|
+
let merged;
|
|
1414
|
+
// 合并可能因块注释 parse 失败抛异常(parseJsonc 边界):降级为「跳过合并、保留原文件」并告警
|
|
1415
|
+
try {
|
|
1416
|
+
merged = mergeGlobalOpencodeJsonc(current);
|
|
1417
|
+
}
|
|
1418
|
+
catch (err) {
|
|
1419
|
+
console.warn(`[update] 全局 opencode.jsonc 解析失败(可能含块注释),跳过合并保留原文件: ${err.message}`);
|
|
1420
|
+
merged = current;
|
|
1421
|
+
}
|
|
1422
|
+
atomicWriteFileSync(globalJsoncPath, merged);
|
|
1423
|
+
return merged;
|
|
1424
|
+
});
|
|
1425
|
+
updateFileHash(newGlobalState, globalJsoncPath, globalJsoncNew);
|
|
1426
|
+
// 4. 项目 opencode.jsonc:不存在则写最小 { $schema };已存在则保留(旧非法字段清理属 stage-39)
|
|
1427
|
+
const projectJsoncPath = resolve(projectPath, 'opencode.jsonc');
|
|
1428
|
+
if (!existsSync(projectJsoncPath)) {
|
|
1429
|
+
atomicWriteFileSync(projectJsoncPath, JSON.stringify(buildProjectOpencodeJsoncObj(), null, 2) + '\n');
|
|
1430
|
+
created.push('opencode.jsonc');
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
skipped.push('opencode.jsonc (use existing)');
|
|
1595
1434
|
}
|
|
1596
|
-
//
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
if (updated.some(f => f.startsWith('.opencode/agents/')) && process.stdout.isTTY) {
|
|
1603
|
-
const lang = getCliLang(projectPath);
|
|
1604
|
-
console.log(lang === 'en'
|
|
1605
|
-
? 'opencode agent configuration updated. Please restart opencode to load the new configuration.'
|
|
1606
|
-
: 'opencode agent 配置已更新,请重启 opencode 以加载新配置。');
|
|
1435
|
+
// 重启提醒(仅在 opencode 全局 agent 配置更新时,且为交互模式)
|
|
1436
|
+
if (updated.some((f) => f.startsWith(getGlobalAgentsDir())) && process.stdout.isTTY) {
|
|
1437
|
+
const reminderLang = getCliLang(projectPath);
|
|
1438
|
+
console.log(reminderLang === 'en'
|
|
1439
|
+
? 'opencode global configuration updated. Please restart opencode to load the new configuration.'
|
|
1440
|
+
: 'opencode 全局配置已更新,请重启 opencode 以加载新配置。');
|
|
1607
1441
|
}
|
|
1608
1442
|
// ── 写入冲突标记文件 ──
|
|
1609
1443
|
if (conflicts.length > 0) {
|
|
1610
1444
|
const openfeelVersion = getOpenfeelVersion();
|
|
1611
|
-
for (const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
? readFileSync(
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1445
|
+
for (const conflictPath of conflicts) {
|
|
1446
|
+
// 全局冲突(绝对路径)写 ~/.openfeel/update_conflicts/;项目冲突写项目 .openfeel/update_conflicts/
|
|
1447
|
+
if (isAbsolute(conflictPath)) {
|
|
1448
|
+
const currentContent = existsSync(conflictPath) ? readFileSync(conflictPath, 'utf-8') : '';
|
|
1449
|
+
const incomingContent = getIncomingContent(conflictPath, lang);
|
|
1450
|
+
const relFromGlobal = relative(getOpencodeGlobalDir(), conflictPath);
|
|
1451
|
+
// 全局冲突根目录 ~/.openfeel/update_conflicts/(与项目 .openfeel/update_conflicts/ 对称)
|
|
1452
|
+
const globalConflictsBase = resolve(dirname(getGlobalUpdateStatePath()), 'update_conflicts');
|
|
1453
|
+
writeConflictFile(projectPath, relFromGlobal, currentContent, incomingContent, openfeelVersion, globalConflictsBase);
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
const absPath = resolve(projectPath, conflictPath);
|
|
1457
|
+
const currentContent = existsSync(absPath) ? readFileSync(absPath, 'utf-8') : '';
|
|
1458
|
+
// 获取 incoming 内容:冲突未实际写入,需单独获取
|
|
1459
|
+
const incomingContent = getIncomingContent(conflictPath, lang);
|
|
1460
|
+
writeConflictFile(projectPath, conflictPath, currentContent, incomingContent, openfeelVersion);
|
|
1461
|
+
}
|
|
1620
1462
|
}
|
|
1621
1463
|
}
|
|
1622
|
-
// ── REV-001
|
|
1464
|
+
// ── REV-001:统一更新双 update_state.json ──
|
|
1623
1465
|
// 规则:
|
|
1624
1466
|
// 1. 无论有无冲突,所有 created/updated 文件的 hash 都必须更新到 state
|
|
1625
1467
|
// 2. 有冲突时,额外标记冲突文件
|
|
1626
1468
|
// 3. 首次 update(state === null)→ 创建新 state
|
|
1627
1469
|
const newState = state ?? createUpdateState(projectPath, {});
|
|
1628
|
-
//
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
if (
|
|
1632
|
-
|
|
1633
|
-
|
|
1470
|
+
// 更新非冲突文件 hash:项目资产(相对路径)→ 项目 state;全局资产(绝对路径)→ 全局 state
|
|
1471
|
+
// appended 追加后同样记录 clean + 新 hash(D38-1:追加即建区,下次走区内替换而非误判无记录)
|
|
1472
|
+
for (const p of [...created, ...updated, ...appended]) {
|
|
1473
|
+
if (isAbsolute(p)) {
|
|
1474
|
+
if (existsSync(p)) {
|
|
1475
|
+
updateFileHash(newGlobalState, p, readFileSync(p, 'utf-8'));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
const absPath = resolve(projectPath, p);
|
|
1480
|
+
if (existsSync(absPath)) {
|
|
1481
|
+
updateFileHash(newState, p, readFileSync(absPath, 'utf-8'));
|
|
1482
|
+
}
|
|
1634
1483
|
}
|
|
1635
1484
|
}
|
|
1636
|
-
//
|
|
1637
|
-
for (const
|
|
1638
|
-
|
|
1485
|
+
// 标记冲突文件(按绝对/相对路由到对应 state)
|
|
1486
|
+
for (const p of conflicts) {
|
|
1487
|
+
if (isAbsolute(p)) {
|
|
1488
|
+
markFileConflict(newGlobalState, p);
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
markFileConflict(newState, p);
|
|
1492
|
+
}
|
|
1639
1493
|
}
|
|
1640
1494
|
// 更新时间戳
|
|
1641
1495
|
newState.last_update = new Date().toISOString();
|
|
1642
|
-
|
|
1496
|
+
newGlobalState.last_update = new Date().toISOString();
|
|
1497
|
+
// 持久化双 state
|
|
1643
1498
|
saveUpdateState(projectPath, newState);
|
|
1644
|
-
|
|
1499
|
+
saveGlobalUpdateState(newGlobalState);
|
|
1500
|
+
return { created, updated, skipped, conflicts, appended };
|
|
1645
1501
|
}
|
|
1646
1502
|
/**
|
|
1647
1503
|
* 写入冲突标记文件到 .openfeel/update_conflicts/{relativePath}
|
|
@@ -1649,10 +1505,11 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1649
1505
|
*
|
|
1650
1506
|
* REV-006:update_conflicts/ 目录已加入 .gitignore,不纳入版本管理。
|
|
1651
1507
|
*/
|
|
1652
|
-
function writeConflictFile(projectPath, relativePath, currentContent, incomingContent, openfeelVersion) {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1508
|
+
function writeConflictFile(projectPath, relativePath, currentContent, incomingContent, openfeelVersion, conflictsBase) {
|
|
1509
|
+
// 全局冲突传全局 base(~/.openfeel),项目冲突默认项目 .openfeel/update_conflicts
|
|
1510
|
+
const base = conflictsBase ?? resolve(projectPath, '.openfeel', 'update_conflicts');
|
|
1511
|
+
mkdirSync(base, { recursive: true });
|
|
1512
|
+
const conflictPath = resolve(base, relativePath);
|
|
1656
1513
|
mkdirSync(dirname(conflictPath), { recursive: true });
|
|
1657
1514
|
const conflictContent = [
|
|
1658
1515
|
`<<<<<<< CURRENT (用户修改版)`,
|
|
@@ -1661,41 +1518,34 @@ function writeConflictFile(projectPath, relativePath, currentContent, incomingCo
|
|
|
1661
1518
|
incomingContent,
|
|
1662
1519
|
`>>>>>>> INCOMING (openfeel v${openfeelVersion} 更新)`,
|
|
1663
1520
|
].join('\n');
|
|
1664
|
-
|
|
1521
|
+
atomicWriteFileSync(conflictPath, conflictContent);
|
|
1665
1522
|
}
|
|
1666
1523
|
/**
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
* -
|
|
1670
|
-
* -
|
|
1671
|
-
* -
|
|
1672
|
-
* - opencode.jsonc →
|
|
1673
|
-
* - AGENTS.md → loadTemplate(lang, 'agents-md')
|
|
1524
|
+
* 根据路径获取 incoming(openfeel 期望写入)内容
|
|
1525
|
+
* relativePath 现为全局绝对路径(框架资产)或项目相对路径(opencode.jsonc):
|
|
1526
|
+
* - getGlobalAgentsMdPath() → loadTemplate(lang, 'agents-md')
|
|
1527
|
+
* - getGlobalAgentsDir()/{name}.md → loadAgentTemplate(lang, name)
|
|
1528
|
+
* - getGlobalSkillsDir()/{name}/SKILL.md → SKILL_DEFINITIONS[name]
|
|
1529
|
+
* - opencode.jsonc → buildProjectOpencodeJsoncObj()
|
|
1674
1530
|
*/
|
|
1675
|
-
function getIncomingContent(
|
|
1676
|
-
//
|
|
1677
|
-
if (relativePath ===
|
|
1678
|
-
return loadTemplate(lang, '
|
|
1531
|
+
function getIncomingContent(relativePath, lang) {
|
|
1532
|
+
// 全局框架约束 AGENTS.md(v1.1.1:约束唯一权威)
|
|
1533
|
+
if (relativePath === getGlobalAgentsMdPath()) {
|
|
1534
|
+
return loadTemplate(lang, 'agents-md');
|
|
1679
1535
|
}
|
|
1680
|
-
//
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
const name = agentMatch[1];
|
|
1536
|
+
// 全局 agent:位于 getGlobalAgentsDir() 下的 {name}.md
|
|
1537
|
+
if (relativePath.startsWith(getGlobalAgentsDir())) {
|
|
1538
|
+
const name = basename(relativePath, '.md');
|
|
1684
1539
|
return loadAgentTemplate(lang, name);
|
|
1685
1540
|
}
|
|
1686
|
-
//
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const name = skillMatch[1];
|
|
1541
|
+
// 全局 skill:位于 getGlobalSkillsDir() 下的 {name}/SKILL.md(跨平台 basename 匹配)
|
|
1542
|
+
if (relativePath.startsWith(getGlobalSkillsDir()) && basename(relativePath) === 'SKILL.md') {
|
|
1543
|
+
const name = basename(dirname(relativePath));
|
|
1690
1544
|
return SKILL_DEFINITIONS[name] ?? '';
|
|
1691
1545
|
}
|
|
1692
|
-
// opencode.jsonc
|
|
1546
|
+
// 项目 opencode.jsonc
|
|
1693
1547
|
if (relativePath === 'opencode.jsonc') {
|
|
1694
|
-
return
|
|
1695
|
-
}
|
|
1696
|
-
// AGENTS.md
|
|
1697
|
-
if (relativePath === 'AGENTS.md') {
|
|
1698
|
-
return loadTemplate(lang, 'agents-md');
|
|
1548
|
+
return JSON.stringify(buildProjectOpencodeJsoncObj(), null, 2) + '\n';
|
|
1699
1549
|
}
|
|
1700
1550
|
// 未知路径 → 空内容(不应发生)
|
|
1701
1551
|
return '';
|