@thincoder/core 0.10.4 → 0.10.5
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/CHANGELOG.md +31 -1
- package/README.md +1 -1
- package/advisor/compaction.mjs +11 -6
- package/advisor/run.mjs +4 -4
- package/agent/assemble.mjs +1 -1
- package/agent/chat-call.mjs +1 -1
- package/agent/child-marks.mjs +2 -2
- package/agent/digest-account.mjs +5 -4
- package/agent/dispatch-gates.mjs +8 -0
- package/agent/dispatch.mjs +39 -18
- package/agent/family-tools.mjs +22 -11
- package/agent/helpers.mjs +4 -3
- package/agent/light-round.mjs +66 -0
- package/agent/post-turn.mjs +8 -4
- package/agent/run-stages.mjs +5 -3
- package/agent/run-start.mjs +3 -2
- package/agent/setup-reminders.mjs +1 -1
- package/agent/setup.mjs +18 -3
- package/agent/subagent-event.mjs +1 -1
- package/agent/suspension.mjs +8 -5
- package/agent/timers.mjs +56 -5
- package/agent/turn-loop.mjs +5 -3
- package/agent/write-gate.mjs +7 -7
- package/agent-tools/advisor-async.mjs +2 -2
- package/agent-tools/advisor-settle.mjs +2 -2
- package/agent-tools/advisor.mjs +1 -1
- package/agent-tools/async-discard.mjs +34 -0
- package/agent-tools/async-settle.mjs +3 -2
- package/agent-tools/audit-block.mjs +1 -1
- package/agent-tools/bash-async.mjs +1 -1
- package/agent-tools/batch-lifecycle.mjs +35 -14
- package/agent-tools/batch-paths.mjs +4 -4
- package/agent-tools/batch-skeleton.mjs +29 -11
- package/agent-tools/batch.mjs +28 -15
- package/agent-tools/browser-async.mjs +198 -0
- package/agent-tools/checkpoint.mjs +1 -1
- package/agent-tools/design-slots.mjs +232 -0
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/manifest.mjs +193 -0
- package/agent-tools/parent-channel.mjs +7 -4
- package/agent-tools/review-facts.mjs +13 -13
- package/agent-tools/settings.mjs +1 -1
- package/agent-tools/skill.mjs +1 -1
- package/agent-tools/subagent-async.mjs +21 -12
- package/agent-tools/subagent-scheduler.mjs +8 -5
- package/agent-tools/subagent-spawn.mjs +29 -55
- package/agent-tools/subagent.mjs +21 -8
- package/agent-tools/timer.mjs +186 -12
- package/agent-tools/zero-write-watch.mjs +2 -2
- package/agent-tools.mjs +2 -1
- package/agent.mjs +1 -1
- package/attachments.mjs +1 -1
- package/config-io.mjs +18 -13
- package/config-migrate.mjs +13 -29
- package/config-presets.mjs +33 -29
- package/config.mjs +68 -25
- package/context-degrade.mjs +1 -1
- package/context-echo.mjs +1 -1
- package/context-push.mjs +2 -2
- package/context.mjs +1 -1
- package/conventions.mjs +2 -2
- package/declaration.mjs +18 -2
- package/file-links.mjs +1 -1
- package/file-refs.mjs +1 -1
- package/history-window.mjs +1 -1
- package/i18n.mjs +2 -0
- package/ledger-cmd.mjs +62 -16
- package/ledger-db.mjs +6 -0
- package/ledger-surface.mjs +49 -5
- package/ledger-tools.mjs +16 -8
- package/ledger.mjs +9 -16
- package/log.mjs +1 -1
- package/manifest-discovery.mjs +1 -1
- package/manifest-schema.mjs +1 -1
- package/mcp/transport-stdio.mjs +1 -1
- package/mcp.mjs +1 -1
- package/memory/code-index.mjs +1 -1
- package/memory/code-search.mjs +1 -1
- package/memory/code-sync.mjs +1 -1
- package/memory/docs.mjs +6 -2
- package/memory/file-list.mjs +1 -1
- package/memory/file-walk.mjs +17 -5
- package/memory/maintain.mjs +410 -0
- package/memory/memory-tool.mjs +21 -6
- package/memory/schema.mjs +19 -7
- package/memory/sweep.mjs +8 -5
- package/memory/sync-tail.mjs +16 -10
- package/model-ref.mjs +16 -13
- package/model-specs.mjs +21 -9
- package/package.json +1 -1
- package/peer-instances.mjs +1 -1
- package/permission.mjs +33 -0
- package/process-probe.mjs +2 -2
- package/prompts/advisor-design.md +4 -2
- package/prompts/advisor-round1.md +1 -1
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +36 -2
- package/prompts/consult-base.md +1 -1
- package/prompts/discipline-engineering.md +32 -15
- package/prompts/discipline-normal.md +46 -2
- package/prompts/persona-eng-designer.md +6 -6
- package/prompts/persona-engineering.md +14 -6
- package/prompts/persona-normal.md +2 -0
- package/prompts/subagent-base.md +3 -0
- package/provider/core.mjs +6 -0
- package/provider/errors.mjs +5 -3
- package/provider/google.mjs +9 -8
- package/provider/list-models.mjs +3 -2
- package/provider/sse.mjs +3 -0
- package/provider-flows.mjs +14 -15
- package/proxy-chunked.mjs +102 -0
- package/proxy-target.mjs +54 -0
- package/proxy-transport.mjs +298 -0
- package/proxy.mjs +17 -248
- package/session-gc.mjs +2 -2
- package/session-lifecycle.mjs +18 -14
- package/session-slot-claims.mjs +2 -2
- package/session-slots-manifest.mjs +1 -1
- package/session-slots.mjs +3 -3
- package/team-ledger.mjs +282 -0
- package/team.mjs +223 -0
- package/tool-docs/bash.md +1 -0
- package/tool-docs/batch.md +1 -1
- package/tool-docs/browser.md +48 -0
- package/tool-docs/ledger.md +3 -3
- package/tool-docs/manifest.md +16 -0
- package/tool-docs/memory.md +1 -0
- package/tool-docs/notify_parent.md +5 -0
- package/tool-docs/process.md +4 -4
- package/tool-docs/question.md +1 -0
- package/tool-docs/subagent.md +4 -3
- package/tool-docs/team_ledger.md +9 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/wait_for.md +1 -0
- package/tools/browser.mjs +161 -0
- package/tools/git.mjs +1 -1
- package/tools/index.mjs +3 -0
- package/tools/ops.mjs +26 -10
- package/tools/repomap.mjs +1 -1
- package/tools/write-path.mjs +2 -2
- package/traces/trace-cleanup.mjs +1 -1
- package/traces/trace-store.mjs +1 -1
- package/vision-reader.mjs +10 -16
package/agent-tools/subagent.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent.mjs — subagent tool(ONE tool,
|
|
2
|
+
* subagent.mjs — subagent tool(ONE tool, NINE actions + spawn 路径驱动器)。
|
|
3
3
|
* 2026-09-07 token 链终消费制:+action: consume-design(ENGINEERING-MODE.md §2.6 F1——
|
|
4
4
|
* 父侧链终核销消费 designId 槽——执行器 executeConsumeDesignAction 在 subagent-spawn.mjs)。
|
|
5
|
+
* 2026-10-07 批 ledger-tool(#927 并入):+action: design-slots(只读清点面——
|
|
6
|
+
* DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1)+ consume-design 选择器扩展(designIds / expired /
|
|
7
|
+
* olderThanDays)——实现在 `design-slots.mjs`(subagent-spawn.mjs re-export 保 import 面)。
|
|
5
8
|
*
|
|
6
9
|
* 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
|
|
7
10
|
*(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
import { buildChildSignal } from "./async-settle.mjs"
|
|
32
35
|
import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
|
|
33
36
|
import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
|
|
37
|
+
import { executeDesignSlotsAction } from "./design-slots.mjs"
|
|
34
38
|
import { executeAsyncSpawn } from "./subagent-run.mjs"
|
|
35
39
|
import { ROUND_VALUES } from "./spawn-gates.mjs"
|
|
36
40
|
// #15 描述外置:描述文本单点 = tool-docs/subagent.md(DESC 单解析面,缺档抛错语义不变)
|
|
@@ -102,14 +106,16 @@ export function buildSyncStoppedReport(role, capturedOutput, designId) {
|
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
/**
|
|
105
|
-
* subagent tool — ONE tool,
|
|
109
|
+
* subagent tool — ONE tool, NINE actions (AGENT-LOOP-SUBAGENT.md §6.7 +
|
|
106
110
|
* SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
|
|
107
111
|
* (inspect a running/queued/done async child's recent activity + current tool — docs/cli/design/TUI.md §6.8) /
|
|
108
112
|
* send (inject a direction into a RUNNING async child — consumed at its next turn
|
|
109
113
|
* boundary as an ordinary instruction — §6.8) / escalate (飞刀 — hand implementation to
|
|
110
114
|
* a stronger model) / cancel (stop ONE background subagent — AGENT-LOOP-SUBAGENT.md §6.7.2) / panel (view + fix
|
|
111
115
|
* the live subagent panel — AGENT-LOOP-SUBAGENT.md §6.7.2) / consume-design (parent-side chain-terminal token
|
|
112
|
-
* consumption — ENGINEERING-MODE.md §2.6, 2026-09-07
|
|
116
|
+
* consumption — ENGINEERING-MODE.md §2.6, 2026-09-07; selectors designId / designIds / expired /
|
|
117
|
+
* olderThanDays — DESIGN-TOKEN-SETTLEMENT.md §10 F-SL2) / design-slots (read-only slot inventory —
|
|
118
|
+
* DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1, 2026-10-07). The check
|
|
113
119
|
* action was deleted (AGENT-LOOP-SUBAGENT.md §6.7.5): async results reach the model only via the auto channel.
|
|
114
120
|
* - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
|
|
115
121
|
* (suitable for codebase exploration); "coder" — full tool set, self-contained
|
|
@@ -130,7 +136,7 @@ export const subagentTool = {
|
|
|
130
136
|
parameters: {
|
|
131
137
|
type: "object",
|
|
132
138
|
properties: {
|
|
133
|
-
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design; semantics per action in the tool description." },
|
|
139
|
+
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send", "design-slots"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design / design-slots; semantics per action in the tool description." },
|
|
134
140
|
view: { type: "boolean", description: "action:'panel' only: true (default) = the live panel blocks as the user sees them; false without freeze = error. Mutually exclusive with freeze (freeze wins)." },
|
|
135
141
|
freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck block — reclaims it into the conversation; refused for running/done/unknown blocks or a still-pending report; works on the CLI TUI panel and on the desktop renderer panel (live readback of the blocks as the user sees them)." },
|
|
136
142
|
task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria)." },
|
|
@@ -138,8 +144,11 @@ export const subagentTool = {
|
|
|
138
144
|
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
139
145
|
model: { type: "string", description: "action:'spawn': provider/model override ('provider:model', a provider, or a model name on the parent's provider) — defaults config.agent.subagentModels[role] → config.agent.subagentModel → the parent's provider; \"default\" inherits. action:'escalate': a consult candidate (default = the first)." },
|
|
140
146
|
designToken: { type: "string", description: "Required when role='eng-coder': the token from advisor(type='design') after the review passed — without it, eng-coder cannot modify files." },
|
|
141
|
-
designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the slot
|
|
142
|
-
|
|
147
|
+
designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the single-slot selector (required to pick between several; the gate refuses to guess); see designIds / expired / olderThanDays for batch consumption." },
|
|
148
|
+
designIds: { type: "array", items: { type: "string" }, description: "action:'consume-design' (batch): an explicit non-empty list of design ids to close out — pass exactly ONE selector per call (designId / designIds / expired / olderThanDays)." },
|
|
149
|
+
expired: { type: "boolean", description: "action:'consume-design' (batch): true = close out every expired slot (exactly ONE selector per call)." },
|
|
150
|
+
olderThanDays: { type: "number", description: "action:'consume-design' (batch): close out every slot older than N days (age > N; exactly ONE selector per call)." },
|
|
151
|
+
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. Refused when absent or when the path does not resolve to a readable file; the content is never validated. role='coder' (normal mode): optional — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file." },
|
|
143
152
|
round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder'/'eng-designer': initial (first round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default — missing = refused." },
|
|
144
153
|
async: { type: "boolean", description: "action:'spawn'/'escalate': default at depth 0 = true (async; the report arrives automatically); depth>0 always sync. async:false forces the blocking flight." },
|
|
145
154
|
files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares — file-level paths only (cwd-relative or absolute; directories rejected — they bypass the conflict detector). Overlapping-file tasks serialize automatically. Omit = no conflict detection." },
|
|
@@ -165,7 +174,7 @@ export const subagentTool = {
|
|
|
165
174
|
// coder+WRITE child (violates explore-only intent) and status/panel/observe/send
|
|
166
175
|
// have no async pool / panel mirror to query in a child context.
|
|
167
176
|
if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
|
|
168
|
-
throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not`)
|
|
177
|
+
throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/design-slots/observe/send are not`)
|
|
169
178
|
}
|
|
170
179
|
// AGENT-LOOP-ASYNC-POOL.md §6.8 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
|
|
171
180
|
// async OR blocking — the digest must stay organize-only. The escalate
|
|
@@ -183,6 +192,10 @@ export const subagentTool = {
|
|
|
183
192
|
// 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
|
|
184
193
|
// 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
|
|
185
194
|
if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
|
|
195
|
+
// 2026-10-07 批 ledger-tool(#927 并入——DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1):design-slots =
|
|
196
|
+
// 只读清点(工程模式父侧限定——门在执行器内);dispatch 分类 = readonly(planMode 放行 /
|
|
197
|
+
// 免审批 / digest 放行——同 status / observe)。
|
|
198
|
+
if (action === "design-slots") return executeDesignSlotsAction(args, ctx)
|
|
186
199
|
// AGENT-LOOP-SUBAGENT.md §6.7.2 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
|
|
187
200
|
// (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
|
|
188
201
|
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
|
|
@@ -195,7 +208,7 @@ export const subagentTool = {
|
|
|
195
208
|
// send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
|
|
196
209
|
if (action === "observe") return executeObserveAction(args, ctx)
|
|
197
210
|
if (action === "send") return executeSendAction(args, ctx)
|
|
198
|
-
throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
|
|
211
|
+
throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send, design-slots.`)
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
const parent = ctx.agent
|
package/agent-tools/timer.mjs
CHANGED
|
@@ -1,10 +1,90 @@
|
|
|
1
|
-
import { TIMER_MAX_PENDING } from "../agent/timers.mjs"
|
|
1
|
+
import { TIMER_MAX_PENDING, TIMER_MIN_EVERY_MS, nextFireAt } from "../agent/timers.mjs"
|
|
2
2
|
import { DESC } from "../tools/shared.mjs"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* timer tool: set a time budget for thinking before the agent insists on action.
|
|
6
|
-
* 描述面 = `tool-docs/timer.md`(DESC() 加载;机制 = AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW8
|
|
6
|
+
* 描述面 = `tool-docs/timer.md`(DESC() 加载;机制 = AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW8 ∥ §6.30.18
|
|
7
|
+
* 四式 + `cancel`/`list`)。取时单点 = 时钟缝 `ctx.now ?? Date.now()`(§6.30.18——生产 `toolCtx`
|
|
8
|
+
* (`agent/dispatch-run.mjs`)不携 `now` ⇒ 缺省回落 = 现行为零变;单元测试档以假 ctx 直驱)。
|
|
7
9
|
*/
|
|
10
|
+
|
|
11
|
+
/** `at` = 本地刻 `YYYY-MM-DDTHH:MM[:SS]`(严格正则+日历回读校验——如 `2026-02-30` 拒)。 */
|
|
12
|
+
const AT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/
|
|
13
|
+
/** `daily` = 本地 `HH:MM`(与核 `DAILY_RE` 同形——工具面 = 唯一生产者,核守卫为防御面)。 */
|
|
14
|
+
const DAILY_RE = /^([01]\d|2[0-3]):([0-5]\d)$/
|
|
15
|
+
|
|
16
|
+
/** 默认 `message` 槽尾(逐式槽 = §6.30.18 默认表;一次性槽逐字零变)。 */
|
|
17
|
+
const DEFAULT_TAIL = ". Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing."
|
|
18
|
+
|
|
19
|
+
/** 本地刻 `YYYY-MM-DD HH:MM`(宿主本地时区——回执 ∥ 默认文案槽共用)。 */
|
|
20
|
+
function localText(ms) {
|
|
21
|
+
const d = new Date(ms)
|
|
22
|
+
const p = (n) => String(n).padStart(2, "0")
|
|
23
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `at` 解算(宿主本地时区):形不合 ∥ 日历回读不符(`2026-02-30` ∥ 时位越界等)⇒ `null`。 */
|
|
27
|
+
function atEpochMs(raw) {
|
|
28
|
+
const m = AT_RE.exec(raw)
|
|
29
|
+
if (m === null) return null
|
|
30
|
+
const [y, mo, d, hh, mi, ss] = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6] ?? 0)]
|
|
31
|
+
const date = new Date(y, mo - 1, d, hh, mi, ss, 0)
|
|
32
|
+
const back = date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d
|
|
33
|
+
&& date.getHours() === hh && date.getMinutes() === mi && date.getSeconds() === ss
|
|
34
|
+
return back ? date.getTime() : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 时长形(§6.30.18):`<mm:ss>`(< 1h)∥ `<h:mm:ss>`(≥ 1h——h 不补零;与 `/timers` 行同判)。 */
|
|
38
|
+
function durText(ms) {
|
|
39
|
+
const sec = Math.max(0, Math.ceil(ms / 1000))
|
|
40
|
+
const h = Math.floor(sec / 3600)
|
|
41
|
+
const mm = String(Math.floor((sec % 3600) / 60)).padStart(2, "0")
|
|
42
|
+
const ss = String(sec % 60).padStart(2, "0")
|
|
43
|
+
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** kind(`list` 行——§6.30.18):`every <N>s` ∕ `daily <HH:MM>` ∕ `at <刻>`(`at: true` 来源标记位——
|
|
47
|
+
* 父侧裁定 2026-10-12);余(一次性)⇒ `one-shot`。 */
|
|
48
|
+
function kindText(t) {
|
|
49
|
+
const everyMs = t?.everyMs
|
|
50
|
+
if (typeof everyMs === "number" && Number.isFinite(everyMs) && everyMs > 0) return `every ${everyMs / 1000}s`
|
|
51
|
+
const daily = t?.daily
|
|
52
|
+
if (typeof daily === "string" && DAILY_RE.test(daily)) return `daily ${daily}`
|
|
53
|
+
if (t?.at === true) return `at ${localText(t.expiresAt)}`
|
|
54
|
+
return "one-shot"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `list` 行第三槽:到期未投 ⇒ `due now`;否则 `next <dur>`(剩余现算)。 */
|
|
58
|
+
function nextText(t, now) {
|
|
59
|
+
const at = Number(t?.expiresAt)
|
|
60
|
+
return at > now ? `next ${durText(at - now)}` : "due now"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `message` 首行(`list` 行第四槽——逐字原样,不截断)。 */
|
|
64
|
+
function firstLine(message) {
|
|
65
|
+
const text = String(message ?? "")
|
|
66
|
+
const i = text.indexOf("\n")
|
|
67
|
+
return i < 0 ? text : text.slice(0, i)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `cancel`(§6.30.18):按 id 移除(`String` 宽松比对);零匹配 ⇒ 显式抛错(文案含 `list` 指引)。 */
|
|
71
|
+
function cancelAction(agent, wanted) {
|
|
72
|
+
const pending = agent?._pendingTimers ?? []
|
|
73
|
+
const idx = pending.findIndex((t) => String(t?.id) === String(wanted))
|
|
74
|
+
if (idx < 0) {
|
|
75
|
+
throw new Error(`timer cancel: no pending timer with id ${String(wanted)} — use list: true to see pending timers`)
|
|
76
|
+
}
|
|
77
|
+
const [hit] = pending.splice(idx, 1)
|
|
78
|
+
return `Timer cancelled (id ${String(hit.id)}).`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `list`(只读枚举——剩余现算;到期未投 ⇒ `due now` 态照出)。 */
|
|
82
|
+
function listAction(agent, now) {
|
|
83
|
+
const pending = agent?._pendingTimers ?? []
|
|
84
|
+
if (pending.length === 0) return "No pending timers."
|
|
85
|
+
return pending.map((t) => `${String(t.id)} · ${kindText(t)} · ${nextText(t, now)} · ${firstLine(t.message)}`).join("\n")
|
|
86
|
+
}
|
|
87
|
+
|
|
8
88
|
export const timerTool = {
|
|
9
89
|
name: "timer",
|
|
10
90
|
description: DESC("timer"),
|
|
@@ -15,6 +95,26 @@ export const timerTool = {
|
|
|
15
95
|
type: "number",
|
|
16
96
|
description: "Thinking budget in seconds (default 180). Longer for complex reasoning, shorter for simple tasks.",
|
|
17
97
|
},
|
|
98
|
+
at: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: 'Fire once at a local clock time, e.g. "2026-10-13T09:00" (YYYY-MM-DDTHH:MM[:SS]). A time already past fires immediately.',
|
|
101
|
+
},
|
|
102
|
+
every: {
|
|
103
|
+
type: "number",
|
|
104
|
+
description: "Fixed interval in seconds (minimum 60): the first reminder comes N seconds after setting, then every N seconds until cancelled.",
|
|
105
|
+
},
|
|
106
|
+
daily: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: 'Local "HH:MM" — repeats every day at that time (a time already past today fires tomorrow), until cancelled.',
|
|
109
|
+
},
|
|
110
|
+
cancel: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Id of a pending timer to stop (from the set confirmation; see also list).",
|
|
113
|
+
},
|
|
114
|
+
list: {
|
|
115
|
+
type: "boolean",
|
|
116
|
+
description: "List pending timers with their next due time (read-only).",
|
|
117
|
+
},
|
|
18
118
|
message: {
|
|
19
119
|
type: "string",
|
|
20
120
|
description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
|
|
@@ -25,22 +125,96 @@ export const timerTool = {
|
|
|
25
125
|
readonly: true,
|
|
26
126
|
sideEffectExempt: true,
|
|
27
127
|
execute(args, ctx) {
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
128
|
+
const now = ctx.now ?? Date.now() // 时钟缝单点(§6.30.18):at 过时判定 ∥ every/daily 首投换算 ∥ id 撞号基准
|
|
129
|
+
const agent = ctx.agent
|
|
130
|
+
|
|
131
|
+
// 动作解析 + 互斥(至多一式;cancel ∥ list 与一切设置式互斥、与彼此互斥)——违例显式拒。
|
|
132
|
+
const hasSeconds = args.seconds !== undefined && args.seconds !== null
|
|
133
|
+
const hasAt = args.at !== undefined && args.at !== null
|
|
134
|
+
const hasEvery = args.every !== undefined && args.every !== null
|
|
135
|
+
const hasDaily = args.daily !== undefined && args.daily !== null
|
|
136
|
+
const hasCancel = args.cancel !== undefined && args.cancel !== null
|
|
137
|
+
const wantsList = args.list === true
|
|
138
|
+
const forms = [hasSeconds && "seconds", hasAt && "at", hasEvery && "every", hasDaily && "daily"].filter((v) => v !== false)
|
|
139
|
+
if (forms.length > 1 || (forms.length > 0 && (hasCancel || wantsList)) || (hasCancel && wantsList)) {
|
|
140
|
+
const given = [...forms, hasCancel && "cancel", wantsList && "list"].filter((v) => v !== false).join(" / ")
|
|
141
|
+
throw new Error(
|
|
142
|
+
`timer: pass at most one action per call — got ${given}. Forms: { seconds } one-shot ∥ { at: "YYYY-MM-DDTHH:MM[:SS]" } absolute ∥ { every: <seconds ≥ 60> } interval ∥ { daily: "HH:MM" } daily ∥ { cancel: <id> } stop ∥ { list: true } list`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
if (hasCancel) return cancelAction(agent, args.cancel)
|
|
146
|
+
if (wantsList) return listAction(agent, now)
|
|
147
|
+
|
|
148
|
+
// 逐式解算(校验逐条显式抛错——不静默钳制;拒点时在途表零变动)
|
|
149
|
+
let form = "seconds"
|
|
150
|
+
let expiresAt = null
|
|
151
|
+
let message = null
|
|
152
|
+
let everySec = null
|
|
153
|
+
let everyMs = null
|
|
154
|
+
let oneShotSec = null
|
|
155
|
+
if (hasAt) {
|
|
156
|
+
form = "at"
|
|
157
|
+
const raw = args.at
|
|
158
|
+
const ts = typeof raw === "string" ? atEpochMs(raw) : null
|
|
159
|
+
if (ts === null) {
|
|
160
|
+
throw new Error(`timer at must be a local clock time "YYYY-MM-DDTHH:MM[:SS]" (e.g. "2026-10-13T09:00"), got: ${String(raw)}`)
|
|
161
|
+
}
|
|
162
|
+
expiresAt = ts // 过时点 ⇒ 所给刻(delay ≤ 0 ⇒ 即时投递——AC-45① 口径;不重排)
|
|
163
|
+
message = `⏰ Time's up (at ${localText(ts)})${DEFAULT_TAIL}`
|
|
164
|
+
} else if (hasEvery) {
|
|
165
|
+
form = "every"
|
|
166
|
+
everySec = Number(args.every)
|
|
167
|
+
if (!Number.isFinite(everySec) || everySec * 1000 < TIMER_MIN_EVERY_MS) {
|
|
168
|
+
throw new Error(`timer every must be a number of seconds ≥ ${TIMER_MIN_EVERY_MS / 1000} (got: ${String(args.every)})`)
|
|
169
|
+
}
|
|
170
|
+
everyMs = everySec * 1000
|
|
171
|
+
expiresAt = nextFireAt({ everyMs, expiresAt: now }, now) // 首投 = 设时 + N(非立即——防「设置即投」双投感)
|
|
172
|
+
message = `⏰ Time's up (every ${everySec} seconds)${DEFAULT_TAIL}`
|
|
173
|
+
} else if (hasDaily) {
|
|
174
|
+
form = "daily"
|
|
175
|
+
if (typeof args.daily !== "string" || !DAILY_RE.test(args.daily)) {
|
|
176
|
+
throw new Error(`timer daily must be a local "HH:MM" time (24-hour, e.g. "09:00"), got: ${String(args.daily)}`)
|
|
177
|
+
}
|
|
178
|
+
expiresAt = nextFireAt({ daily: args.daily }, now) // 今日该刻未过 ⇒ 今日;已过 ⇒ 明日(非即时)
|
|
179
|
+
message = `⏰ Time's up (daily at ${args.daily})${DEFAULT_TAIL}`
|
|
180
|
+
} else {
|
|
181
|
+
// D15.3#1:默认 180;非法值(非数字/非正数)显式报错——静默 NaN 定时器永不触发的坑
|
|
182
|
+
const seconds = Number(args.seconds ?? 180)
|
|
183
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
184
|
+
throw new Error(`timer seconds must be a positive number, got: ${args.seconds}`)
|
|
185
|
+
}
|
|
186
|
+
expiresAt = now + seconds * 1000
|
|
187
|
+
message = `⏰ Time's up (${seconds}s)${DEFAULT_TAIL}`
|
|
188
|
+
oneShotSec = seconds
|
|
32
189
|
}
|
|
33
|
-
const expiresAt = Date.now() + seconds * 1000
|
|
34
|
-
const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
|
|
35
190
|
|
|
36
191
|
// §6.30.3 门三件②(D-TW6):在途帽 8——超限显式拒(不静默丢 / 不静默清;形态先例 = 队满
|
|
37
|
-
// 「拒 + 提示 +
|
|
38
|
-
const pending = (
|
|
192
|
+
// 「拒 + 提示 + 保留」)。周期项同占坑(1 条 1 坑——§6.30.18 D-TW15;重排净零不触帽)。
|
|
193
|
+
const pending = (agent._pendingTimers = agent._pendingTimers ?? [])
|
|
39
194
|
if (pending.length >= TIMER_MAX_PENDING) {
|
|
40
195
|
throw new Error(`timer: ${pending.length} timers already pending (max ${TIMER_MAX_PENDING}) — wait for one to fire before setting another`)
|
|
41
196
|
}
|
|
42
|
-
|
|
197
|
+
// id 唯一化(取消可寻址·§6.30.18):基准读时钟缝单点,与在途撞号 ⇒ `+1` 递增至空闲(零新载体字段)。
|
|
198
|
+
let id = now
|
|
199
|
+
while (pending.some((t) => String(t?.id) === String(id))) id += 1
|
|
200
|
+
const entry = { id, expiresAt, message: args.message || message }
|
|
201
|
+
if (everyMs !== null) entry.everyMs = everyMs
|
|
202
|
+
if (form === "daily") entry.daily = args.daily
|
|
203
|
+
if (form === "at") entry.at = true // 来源标记位(非载重——时刻单源 = `expiresAt`;父侧裁定 2026-10-12)
|
|
204
|
+
pending.push(entry)
|
|
43
205
|
|
|
44
|
-
|
|
206
|
+
if (form === "at") {
|
|
207
|
+
const when = localText(expiresAt)
|
|
208
|
+
return expiresAt > now
|
|
209
|
+
? `Timer set (id ${id}) for ${when} local. Stop it with cancel id ${id}.`
|
|
210
|
+
: `Timer set (id ${id}) for ${when} local — that time has passed, so the reminder appears immediately.`
|
|
211
|
+
}
|
|
212
|
+
if (form === "every") {
|
|
213
|
+
return `Repeating timer set (id ${id}): first reminder in ${everySec} seconds, then every ${everySec} seconds. Stop it with cancel id ${id}.`
|
|
214
|
+
}
|
|
215
|
+
if (form === "daily") {
|
|
216
|
+
return `Daily timer set (id ${id}): next reminder ${localText(expiresAt)} local, then every day at ${args.daily}. Stop it with cancel id ${id}.`
|
|
217
|
+
}
|
|
218
|
+
return `Timer set for ${oneShotSec} seconds. A reminder will appear when time is up.`
|
|
45
219
|
},
|
|
46
220
|
}
|
|
@@ -36,7 +36,7 @@ export function advisorAlertText(from, rounds) {
|
|
|
36
36
|
* 命中 ⇒ `pushChildUpstream({parent, from: _upstream.label, kind: "note", message})` + 置闩。
|
|
37
37
|
* **恒零抛**(兜底捕获兑现——自动源无模型可报错;失败面静默跳过)。
|
|
38
38
|
* @param {object} child 子 agent(eng-coder / eng-designer 形态;`_upstream` 由 spawn 站点装配)。
|
|
39
|
-
* @returns {boolean}
|
|
39
|
+
* @returns {boolean} 是否命中推送(测试)。
|
|
40
40
|
*/
|
|
41
41
|
export function maybeZeroWriteAlert(child) {
|
|
42
42
|
try {
|
|
@@ -65,7 +65,7 @@ export function maybeZeroWriteAlert(child) {
|
|
|
65
65
|
* @param {object} parent 父 agent(接收方——`pushChildUpstream` 载体吸收同款)。
|
|
66
66
|
* @param {string} from 来源标签(`advisor#<entry id>`)。
|
|
67
67
|
* @param {number} rounds 连续无产出轮数。
|
|
68
|
-
* @returns {boolean}
|
|
68
|
+
* @returns {boolean} 是否推送成功(测试)。
|
|
69
69
|
*/
|
|
70
70
|
export function pushAdvisorAlert(parent, from, rounds) {
|
|
71
71
|
try {
|
package/agent-tools.mjs
CHANGED
|
@@ -13,13 +13,14 @@ export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
|
|
|
13
13
|
export { timerTool } from "./agent-tools/timer.mjs"
|
|
14
14
|
export { advisorTool } from "./agent-tools/advisor.mjs"
|
|
15
15
|
export { engTool } from "./agent-tools/eng.mjs"
|
|
16
|
+
export { manifestTool } from "./agent-tools/manifest.mjs"
|
|
16
17
|
export { readHistoryTool } from "./agent-tools/read-history.mjs"
|
|
17
18
|
// 模型主动整理上下文(CONTEXT-COMPACTION.md §6.16 · F-CC5):单工具三操作 stats / prune / compact
|
|
18
19
|
// ——depth-0 段挂载(子代理面裁见 §6.16.6 `family-tools.mjs` `depthOnly`)。
|
|
19
20
|
export { contextTool } from "./agent-tools/context.mjs"
|
|
20
21
|
export { batchTool } from "./agent-tools/batch.mjs"
|
|
21
22
|
// 批次档生命周期工具(BATCH-RECORD §4.3 挂载表——主名 `batch` 单工具四 action:depth-0 主 agent
|
|
22
|
-
// create/close + append §1/§4/§6
|
|
23
|
+
// create/close + append/status 域随模式(工程面 = append §1/§4/§6 ∥ status §1;普通面 = append §1/§2/§4/§5/§6 ∥ status §1/§2/§5)(轮 2 裁定②:§4/§6 状态面走普通文档写)(D-BR18);
|
|
23
24
|
// eng 子代理 eng 分支绑定段写入;评审 §3。
|
|
24
25
|
// CORE-UNIFICATION TOOLS #83(统一登记册——VSC `agent-tools/index.mjs:15` 含 consult 家族;
|
|
25
26
|
// CLI 原把 consult 另挂 `agent/setup.mjs:173` ⇒ 归位:登记册即单一来源,装配方只读本档)。
|
package/agent.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
91
91
|
try {
|
|
92
92
|
return await runTurnLoop(agent, {
|
|
93
93
|
maxTurns, threshold, toolSchemas, toolByName, systemPrompt,
|
|
94
|
-
depth, signal, autoTurn, streamOutput, consumeInjected, consumeQueuedInput, callbacks, distillSignal,
|
|
94
|
+
depth, signal, autoTurn, suspDriven, streamOutput, consumeInjected, consumeQueuedInput, callbacks, distillSignal,
|
|
95
95
|
})
|
|
96
96
|
} catch (e) {
|
|
97
97
|
thrownError = e
|
package/attachments.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export function isNonVisionModel(model) {
|
|
|
64
64
|
*/
|
|
65
65
|
export function savePastedImages(dataUrls, cwd, { fs } = {}) {
|
|
66
66
|
const list = Array.isArray(dataUrls) ? dataUrls : []
|
|
67
|
-
if (list.length === 0) return { paths: [], dropped: 0 } //
|
|
67
|
+
if (list.length === 0) return { paths: [], dropped: 0 } // 空表先于缝校验早退(零判据 —— 缝不检)
|
|
68
68
|
if (typeof fs?.mkdirSync !== "function" || typeof fs?.writeFileSync !== "function") {
|
|
69
69
|
throw new TypeError("savePastedImages: fs seam missing — inject { mkdirSync, writeFileSync } (the write face must not silently drop every image)")
|
|
70
70
|
}
|
package/config-io.mjs
CHANGED
|
@@ -141,7 +141,7 @@ const warnedContext = new Set()
|
|
|
141
141
|
/**
|
|
142
142
|
* Resolve providers list + active name from disk(VSC `config-io.mjs` 逐字随迁)。BaseURL
|
|
143
143
|
* trailing slashes normalized. `activeProvider` 语义 = config.defaultModel 的渠道;缺失/失效
|
|
144
|
-
* → 回退首 provider。渠道单值 `model`
|
|
144
|
+
* → 回退首 provider。渠道单值 `model` 一律删除(2026-10-09 清除批——渠道不携模型,零回落)。
|
|
145
145
|
*/
|
|
146
146
|
export function resolveProviders() {
|
|
147
147
|
const raw = loadRaw()
|
|
@@ -150,8 +150,8 @@ export function resolveProviders() {
|
|
|
150
150
|
: []
|
|
151
151
|
for (const p of providers) {
|
|
152
152
|
if (typeof p.baseURL === "string") p.baseURL = p.baseURL.replace(/\/+$/, "")
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// 2026-10-09 清除批(PROVIDER.md §6.16 M3):渠道不携模型——一律删,不落任何回退
|
|
154
|
+
delete p.model
|
|
155
155
|
// PROVIDER.md §15 D-C1: providers[].context must be a positive integer (K units).
|
|
156
156
|
if (p.context != null) {
|
|
157
157
|
const n = Number(p.context)
|
|
@@ -218,9 +218,11 @@ export function removeProviderKeyFromConfig(name) {
|
|
|
218
218
|
|
|
219
219
|
const FORMATS = ["openai", "anthropic", "google"]
|
|
220
220
|
|
|
221
|
-
/** 新增渠道。payload: { preset?: name, custom?: { name, baseURL,
|
|
221
|
+
/** 新增渠道。payload: { preset?: name, custom?: { name, baseURL, format }, key?, proxy? }
|
|
222
|
+
* `proxy === true` ⇒ 同批落 `proxy: true`(缺 ∥ 非真 ⇒ 零键——与行面 `handleSetProviderProxy` 同判据)。
|
|
223
|
+
* 2026-10-09 清除批:custom 不携 `model`——必填判 ∥ 落键双退(渠道不携模型——R3)。
|
|
222
224
|
* 返回错误串或 null。 */
|
|
223
|
-
export function addProviderEntry({ preset, custom, key } = {}) {
|
|
225
|
+
export function addProviderEntry({ preset, custom, key, proxy } = {}) {
|
|
224
226
|
let providers
|
|
225
227
|
try {
|
|
226
228
|
({ providers } = resolveProviders())
|
|
@@ -240,22 +242,25 @@ export function addProviderEntry({ preset, custom, key } = {}) {
|
|
|
240
242
|
if (existing.has(name) || PROVIDER_PRESETS[name]) return `Name "${name}" is already in use`
|
|
241
243
|
const baseURL = (custom.baseURL || "").trim().replace(/\/+$/, "")
|
|
242
244
|
if (!baseURL) return "Base URL is required"
|
|
243
|
-
const model = (custom.model || "").trim()
|
|
244
|
-
if (!model) return "Model is required"
|
|
245
245
|
const format = (custom.format || "openai").trim()
|
|
246
246
|
if (!FORMATS.includes(format)) return `Unknown API format: ${format} (expected ${FORMATS.join("/")})`
|
|
247
|
-
entry = { name, baseURL
|
|
247
|
+
entry = { name, baseURL } // 2026-10-09 清除批:渠道不携模型——单值 model 双退(R3 零读写)
|
|
248
248
|
if (format !== "openai") entry.format = format
|
|
249
249
|
} else {
|
|
250
250
|
return "Add provider needs a preset or a custom config"
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
// #1027(三端对齐批):走 proxy 旗——仅真值落键(运行期 = 逐渠 `proxy: true` ∧ 代理 `uri` 在案——无全局闸;2026-10-08)
|
|
254
|
+
if (proxy === true) entry.proxy = true
|
|
255
|
+
|
|
256
|
+
// #1062(PROVIDER.md §6.23):落条与置钥合为**一次**写——单写原子(不被两段写撕开 ⇒ 半状态
|
|
257
|
+
// 不可达);F5b 冲突 ⇒ 提示串且零落盘(条目 ∥ 钥俱不落)。
|
|
256
258
|
const k = (key || "").trim()
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
const r = persistRaw((raw) => {
|
|
260
|
+
(raw.providers ??= []).push(entry)
|
|
261
|
+
if (k) entry.apiKey = k
|
|
262
|
+
})
|
|
263
|
+
return conflictError(r)
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
/** 移除渠道。激活渠道受保护(CLI parity)。返回错误串或 null。 */
|
package/config-migrate.mjs
CHANGED
|
@@ -5,17 +5,14 @@
|
|
|
5
5
|
* the caller (CLI: config.mjs loadConfig; VSC: config-io.mjs loadRaw — same rule, 双端各自
|
|
6
6
|
* 独立实现——不做同步依赖).
|
|
7
7
|
*
|
|
8
|
-
* 目标形态 C
|
|
8
|
+
* 目标形态 C(2026-10-09 清除批:渠道单值模型退场——M7 v2):渠道**不携模型**(`providers[].model` /
|
|
9
|
+
* `models[]` 一律删)+ 顶层 `defaultModel` 复合(新会话起点)。
|
|
9
10
|
* 老形态迁移(v2):
|
|
10
|
-
* - A:`
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
* - 混合/垃圾:非数组 `p.models` / 非字符串(空串)`p.model` → 删除(清理)
|
|
15
|
-
* - 顺序:先构造/读取有效 `defaultModel` 值(含老 active* 转换),再按它给各渠道播种 `p.model`
|
|
16
|
-
* - 幂等:无老字段(无 `models` / 无 active* / 无垃圾值)→ 返回 false 不动;slot 旧字段
|
|
11
|
+
* - A:`activeProvider/activeModel` → `defaultModel` 复合(`activeModel` 优先;AP 不存在/AM 空 → 首渠道回退)
|
|
12
|
+
* - B:`providers[].models[]` / `providers[].model` → 一律删除(清理)
|
|
13
|
+
* - 顺序:先构造/读取有效 `defaultModel` 值(含老 active* 转换),再清各渠道 `model` / `models` 键
|
|
14
|
+
* - 幂等:无老字段(无 `models` / 无 `model` / 无 active*)→ 返回 false 不动;slot 旧字段
|
|
17
15
|
* (会话文件内)不迁移——读侧容忍
|
|
18
|
-
* - 空结果合法:渠道无模型来源 → `p.model` 不设(模型选择经 `/models` 拉取候选——M8/M9)
|
|
19
16
|
*
|
|
20
17
|
* ── 核内合并(CORE-UNIFICATION §2.5 #79 · CONFIG 组「取并集」)──
|
|
21
18
|
* ② `migrateCore`(VS Code 旧设置 / 密钥库迁移遍——VSC 侧逐字随迁 + 依赖面收核为注入参数):
|
|
@@ -46,25 +43,12 @@ export function migrateLegacyModelFields(raw) {
|
|
|
46
43
|
changed = true
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
// ── ②
|
|
50
|
-
|
|
51
|
-
const dmSep = dm ? dm.indexOf(":") : -1
|
|
52
|
-
const dmProvider = dmSep > 0 ? dm.slice(0, dmSep) : null
|
|
53
|
-
const dmModel = dmSep > 0 && dmSep < dm.length - 1 ? dm.slice(dmSep + 1) : null
|
|
46
|
+
// ── ② 渠道模型键清除(2026-10-09 清除批——M7 v2:渠道不携模型,`model` / `models` 一律删;
|
|
47
|
+
// 幂等 = 无键零改)──
|
|
54
48
|
for (const p of providers) {
|
|
55
49
|
if (!p || typeof p !== "object") continue
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
? dmModel
|
|
59
|
-
: (typeof p.model === "string" && p.model.trim() ? p.model : seedModel(p))
|
|
60
|
-
if (next == null) delete p.model
|
|
61
|
-
else p.model = next
|
|
62
|
-
delete p.models
|
|
63
|
-
changed = true
|
|
64
|
-
continue
|
|
65
|
-
}
|
|
66
|
-
if (p.models !== undefined) { delete p.models; changed = true } // 非数组 models —— 垃圾清理
|
|
67
|
-
if (p.model !== undefined && !(typeof p.model === "string" && p.model.trim())) { delete p.model; changed = true }
|
|
50
|
+
if (p.models !== undefined) { delete p.models; changed = true }
|
|
51
|
+
if (p.model !== undefined) { delete p.model; changed = true }
|
|
68
52
|
}
|
|
69
53
|
return changed
|
|
70
54
|
}
|
|
@@ -93,7 +77,7 @@ function seedModel(p) {
|
|
|
93
77
|
*
|
|
94
78
|
* Never overwrites an apiKey already present in config.json (the CLI may have written it first).
|
|
95
79
|
* Builtin preset names missing from providers[] are created from PROVIDER_PRESETS; unknown
|
|
96
|
-
* names are only created when baseURL
|
|
80
|
+
* names are only created when baseURL metadata is recoverable (渠道不携模型——2026-10-09 清除批).
|
|
97
81
|
*/
|
|
98
82
|
export async function migrateCore(deps) {
|
|
99
83
|
const { secrets, flags, legacySettings, clearLegacySettings } = deps
|
|
@@ -122,8 +106,8 @@ export async function migrateCore(deps) {
|
|
|
122
106
|
entry = presetToEntry(name)
|
|
123
107
|
} else {
|
|
124
108
|
const meta = legacyMeta[name]
|
|
125
|
-
if (!meta?.baseURL
|
|
126
|
-
entry = { name, baseURL: meta.baseURL
|
|
109
|
+
if (!meta?.baseURL) return // orphan key, no way to reconstruct(渠道不携模型——2026-10-09 清除批:门只认 baseURL,model 零读零落)
|
|
110
|
+
entry = { name, baseURL: meta.baseURL }
|
|
127
111
|
// PROVIDER.md §15 D-C4: migrate-settings is the same source — a legacy
|
|
128
112
|
// settings entry carrying context (K units) rides along into config.json.
|
|
129
113
|
if (meta.context != null) entry.context = meta.context
|