@thincoder/core 0.10.1 → 0.10.2

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 CHANGED
@@ -3,6 +3,19 @@
3
3
  All notable changes to the core package are documented here.
4
4
  Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
5
5
 
6
+ ## [0.10.2] — 2026-10-03
7
+
8
+ > 0.10.1 → 0.10.2(月内 +1——发布时定号)
9
+
10
+ ### Added
11
+
12
+ - **统一 provider 解析(#841 · 三端归一)**:核导出 `resolveProviderPlan`(回退链 = 槽渠道(持 key)→ `defaultModel` 渠道 → 首个持 key 渠道;模型面 = 槽模型 → `defaultModel`(同渠道)→ 渠道单值 → `null`;三态 = `ok` ∕ `fallback` ∕ `invalid` + `reason`)与 `resolveChannelModel`(单值转口面)。
13
+ - **`providerState` ∕ `providerStateReason` 两键**:`loadConfig()` 落(`provider` = 解析入选渠道)+ 槽面 key 门(`applySession`——槽无 key ⇒ 跳过且不写槽)。
14
+
15
+ ### Changed
16
+
17
+ - **「无效」语义收窄**:仅「无 provider ∥ 无 key」为 `invalid`(不可运行 ⇒ 引导配置);「可运行而无有效 `defaultModel`」为 `fallback`(**可运行 + 必明示**——不再当无效拦下)。
18
+
6
19
  ## [0.10.1] — 2026-10-03
7
20
 
8
21
  > 0.9.5 → 0.10.1(月切换——十月首发,发布时定号)
package/config.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
15
15
  import { dirname, join } from "node:path"
16
- import { parseModelRef, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
16
+ import { parseModelRef, resolveChannelModel, resolveProviderPlan, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
17
17
  // 老形态迁移核(M7 v2——纯函数零依赖——本文件超 500 行硬限拆分,VSC 同构文件)
18
18
  import { migrateLegacyModelFields } from "./config-migrate.mjs"
19
19
  import { expandHome } from "./expand-home.mjs"
@@ -23,7 +23,7 @@ import {
23
23
  _configPath, writeConfigAtomic,
24
24
  } from "./config-io.mjs"
25
25
 
26
- export { parseModelRef, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
26
+ export { parseModelRef, resolveChannelModel, resolveProviderPlan, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
27
27
  export { PROVIDER_PRESETS, presetToEntry, configDir, configPath, _setConfigPathForTest, _resetConfigPathForTest }
28
28
  // 核内单一写盘执行体(已迁 `config-io.mjs`)——本档 re-export 保持既有导入面(§2.5 #131)。
29
29
  export { writeConfigAtomic }
@@ -325,14 +325,18 @@ export function loadConfig() {
325
325
  // 保证 agent.config.proxy 永远是规范形态或 undefined
326
326
  merged.proxy = normalizeProxy(merged.proxy)
327
327
 
328
- // Runtime provider = config.defaultModel 复合解析(F-2——resolveRuntimeProvider)。
329
- // 无效/未设 → {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打 _providerInvalid
330
- // 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用 findProvider
331
- // throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
332
- merged.provider = resolveRuntimeProvider(merged.providers, merged.defaultModel)
328
+ // Runtime provider = 统一解析(#841——resolveProviderPlan 单源:回退序 + 三态;PROVIDER.md §6.22)。
329
+ // 不可运行(渠表空 ∥ 全表无 key)⇒ {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打
330
+ // _providerInvalid 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用
331
+ // findProvider throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
332
+ const plan = resolveProviderPlan({ providers: merged.providers, defaultModel: merged.defaultModel })
333
+ merged.provider = plan.provider
334
+ merged.providerState = plan.state // ok ∥ fallback ∥ invalid(三值闭集——KD-841-3 config 级)
335
+ merged.providerStateReason = plan.reason // state≠ok 时非空(语义源;消费者按 state 出词——禁串嗅探)
336
+ // providerInvalidReason 语义不变(provider 不完整时非空——装配校验 validateProvider 消费面照旧)
333
337
  merged.providerInvalidReason = merged.provider.name
334
338
  ? null
335
- : defaultModelReason(merged.providers, merged.defaultModel)
339
+ : plan.reason ?? defaultModelReason(merged.providers, merged.defaultModel)
336
340
 
337
341
  // Compaction threshold follows the model (provider-level context override honored — providerSpec)
338
342
  const explicitThreshold = config.agent?.compactThreshold
package/model-ref.mjs CHANGED
@@ -11,15 +11,23 @@
11
11
  * are gone with activeProvider/activeModel (F-1), so a bare provider or bare model is
12
12
  * invalid everywhere (裁定③——裸 provider 拒——显式 p:m)。
13
13
  *
14
- * Runtime model resolution:
14
+ * Runtime model resolution (#841 单源 = resolveProviderPlan——机制全文 PROVIDER.md §6.22):
15
15
  * parseModelRef(ref, providers) → { ok, provider, model } | { ok:false, reason }
16
+ * resolveChannelModel(entry, defaultModel)
17
+ * → 渠道模型面单值:defaultModel 属本渠道 ⇒ 其模型段 ∥
18
+ * 渠道单值 `entry.model` ∥ null(VSC 转口面)
19
+ * resolveProviderPlan({ providers, defaultModel, slot })
20
+ * → { state, source, channel, model, provider, reason }
21
+ * 统一回退解析(纯函数 · 零 I/O——三端消费同一函数):
22
+ * 渠道面回退序三步 + 模型面四步 + 三态 ok ∥ fallback ∥ invalid
16
23
  * resolveRuntimeProvider(providers, defaultModel)
17
24
  * → provider object with `.model` set, or {} when
18
25
  * defaultModel is null/invalid (D-S1: callers mark
19
26
  * _providerInvalid — never throws)
20
27
  * defaultModelReason(providers, defaultModel) → human reason for the D-S1 marker
21
28
  *
22
- * Consumers: config.mjs loadConfig (re-exported as the config hub), make-agent.mjs.
29
+ * Consumers: config.mjs loadConfig (re-exported as the config hub), make-agent.mjs,
30
+ * session-lifecycle.mjs applySession(槽面), VSC panel-turn-stages ∥ presets(核转口)。
23
31
  */
24
32
  export function parseModelRef(ref, providers) {
25
33
  if (typeof ref !== "string" || !ref.trim()) {
@@ -47,7 +55,9 @@ export function parseModelRef(ref, providers) {
47
55
  /** Resolve the runtime provider object for config.defaultModel: provider clone carrying
48
56
  * `.model` = the parsed concrete model (API/spec consumers read provider.model unchanged).
49
57
  * Invalid/missing defaultModel → {} (D-S1 shape — name/model/baseURL absent so
50
- * validateProvider flags it; TUI/headless surface the reason from defaultModelReason). */
58
+ * validateProvider flags it; TUI/headless surface the reason from defaultModelReason).
59
+ * #841 后:运行时取值 = `resolveProviderPlan`(回退序 + 三态);本函数保留为**严格单发**解析面
60
+ * (零回退——解析未过即 `{}`),新码勿调。 */
51
61
  export function resolveRuntimeProvider(providers, defaultModel) {
52
62
  const r = parseModelRef(defaultModel, providers)
53
63
  if (!r.ok) return {}
@@ -64,3 +74,86 @@ export function defaultModelReason(providers, defaultModel) {
64
74
  }
65
75
  return parseModelRef(defaultModel, list).reason ?? `defaultModel "${defaultModel}" 无效`
66
76
  }
77
+
78
+ /** 「持 key」判据(#841——`providers[].apiKey` trim 后非空;env 变量不是密钥源,config.mjs 档头同判)。 */
79
+ function hasKey(entry) {
80
+ return typeof entry?.apiKey === "string" && entry.apiKey.trim() !== ""
81
+ }
82
+
83
+ /** 渠道模型面单值(#841 模型面 ②③——VSC `resolveDefaultModel` 转口面 · 纯函数):
84
+ * ① `defaultModel` 解析通过 ∧ 其渠道 == `entry` ⇒ 其模型段(首冒号分割 · 双段非空 · name 相等);
85
+ * ② 渠道单值 `entry.model`(非空串);③ `null`(合法——模型由运行期 `/models` 候选 ∥ 用户选择决定)。
86
+ * @param {object|null} entry — providers[] 条目(或同形对象)
87
+ * @param {string|null} defaultModel — 顶层复合串
88
+ * @returns {string|null} */
89
+ export function resolveChannelModel(entry, defaultModel) {
90
+ if (entry?.name) {
91
+ const ref = parseModelRef(defaultModel, [entry])
92
+ if (ref.ok) return ref.model
93
+ }
94
+ return typeof entry?.model === "string" && entry.model ? entry.model : null
95
+ }
96
+
97
+ /** 运行时渠道/模型统一解析(#841 核单源——机制全文 PROVIDER.md §6.22;纯函数 · 零 I/O)。
98
+ * 三端(CLI ∥ VSC ∥ 桌面)消费同一函数:`loadConfig` 运行时 provider 取值 = 本函数产物(CLI ∥ 桌面
99
+ * 经装配自动同源);VSC 回合面直调(接入面自建链收正)。
100
+ *
101
+ * **渠道面回退序**:① 会话槽渠道(`slot.provider` 在场 ∧ 在册 ∧ 持 key ⇒ `source="slot"`);
102
+ * ② defaultModel 渠道(解析通过 ∧ 该渠道持 key ⇒ `source="defaultModel"`);③ 首个持 key 渠道
103
+ * (按表序 ⇒ `source="registry"`);④ 无 ⇒ `state="invalid"`(渠表空 ∥ 全表无 key)。
104
+ * 槽无 key ⇒ **跳过**(不把不可运行渠道钉进运行态)。
105
+ * **模型面**:① 入选来源 = 槽 ∧ 槽带模型 ⇒ 槽模型;② defaultModel 属入选渠道 ⇒ 其模型段;
106
+ * ③ 入选渠道单值;④ 无 ⇒ `null`(消费者沿既有「model 缺失」处置;明示词形随缺 = 仅渠道名)。
107
+ * **三态(config 级——KD-841-3:槽改写「谁在跑」,不改「默认模型是否有效」)**:`ok` =
108
+ * defaultModel 独立成立(解析通过 ∧ 该渠道持 key)⇒ 零明示;`fallback` =「无有效 defaultModel」类
109
+ * ∧ 全局有可运行渠道 ⇒ 可运行 + 明示必达;`invalid` =「无 provider/key」类 ⇒ 真无效 + 引导配置。
110
+ * `reason` = state≠ok 的语义源(消费者按 `state` 出词——**禁串嗅探**,非契约)。
111
+ * @param {{providers?: object[], defaultModel?: string|null, slot?: {provider?: string|null, model?: string|null}|null}} [input]
112
+ * @returns {{state: "ok"|"fallback"|"invalid", source: "slot"|"defaultModel"|"registry"|null, channel: string|null, model: string|null, provider: object, reason: string|null}} */
113
+ export function resolveProviderPlan({ providers, defaultModel, slot } = {}) {
114
+ const list = Array.isArray(providers) ? providers : []
115
+ const slotName = typeof slot?.provider === "string" && slot.provider ? slot.provider : null
116
+ const slotModel = typeof slot?.model === "string" && slot.model ? slot.model : null
117
+
118
+ // ── 渠道面回退序(三步)──
119
+ let entry = null
120
+ let source = null
121
+ if (slotName) {
122
+ const hit = list.find((p) => p?.name === slotName)
123
+ if (hit && hasKey(hit)) { entry = hit; source = "slot" } // 槽无 key ⇒ 跳过(落下一档)
124
+ }
125
+ const ref = parseModelRef(defaultModel, list)
126
+ if (!entry && ref.ok && hasKey(ref.provider)) { entry = ref.provider; source = "defaultModel" }
127
+ if (!entry) {
128
+ const first = list.find((p) => hasKey(p))
129
+ if (first) { entry = first; source = "registry" }
130
+ }
131
+
132
+ // ── 模型面(四步;无入选渠道 ⇒ null)──
133
+ const model = entry ? (source === "slot" && slotModel ? slotModel : resolveChannelModel(entry, defaultModel)) : null
134
+
135
+ // ── 三态(config 级)──
136
+ const dmOk = ref.ok && hasKey(ref.provider)
137
+ const state = !entry ? "invalid" : dmOk ? "ok" : "fallback"
138
+
139
+ // ── reason(语义源 · 逐档)──
140
+ let reason = null
141
+ if (state === "invalid") {
142
+ reason = list.length === 0
143
+ ? "未配置任何 provider"
144
+ : "未配置 API 密钥(providers[].apiKey)——请先配置渠道密钥"
145
+ } else if (state === "fallback") {
146
+ reason = ref.ok
147
+ ? `defaultModel 渠道 "${ref.provider.name}" 无 API 密钥——已回退到可用渠道`
148
+ : defaultModelReason(list, defaultModel)
149
+ }
150
+
151
+ return {
152
+ state,
153
+ source,
154
+ channel: entry?.name ?? null,
155
+ model,
156
+ provider: entry ? { ...entry, model } : {},
157
+ reason,
158
+ }
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
5
  "keywords": [
6
6
  "ai",
@@ -19,6 +19,8 @@
19
19
 
20
20
  import { existsSync } from "node:fs"
21
21
  import { resolveCompactThreshold } from "./config.mjs"
22
+ // 槽面判据单源(#841 KD-841-2:槽渠道 key 门 + 模型面序——PROVIDER.md §6.22)。
23
+ import { resolveProviderPlan } from "./model-ref.mjs"
22
24
  import {
23
25
  slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile, getSessionId,
24
26
  writeEndMarker, sessionEnd, ownerPid, ownerPids,
@@ -158,18 +160,25 @@ export function applySession(agent, data, opts = {}) {
158
160
  } else {
159
161
  unbindRecordStore(agent)
160
162
  }
161
- // ── MODEL-MERGE-SESSION 恢复(F-4——两支)──
162
- const slotProvider = data.activeProvider ? agent.providers?.find((pr) => pr.name === data.activeProvider) : null
163
- if (slotProvider) {
164
- // ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空——legacy 槽 activeModel
165
- // null/空串 = 无 override——回该渠道默认模型(`providers[].model` 单值——M3)。
166
- // F-2d(MODEL-400-FIX):`||` 非 `??`——空串也兜(activeModel="" 的槽会让 `??` 不落链 →
167
- // 空槽 model 恒有值,provider.model 键不缺失)
163
+ // ── MODEL-MERGE-SESSION 恢复(F-4——两支;#841:槽面判据单源 = 统一解析第 1 步)──
164
+ // 槽渠道(`activeProvider` 在册 ∧ 持 key)⇒ 槽值设;槽无 key ∥ 不在册 ⇒ 跳过(落 config 链——
165
+ // 不把不可运行渠道钉进运行态;**跳过不写槽**——原槽值保留(D-S3 / KD-841-2))。判定 + 模型面
166
+ // 同源 = `resolveProviderPlan`(PROVIDER.md §6.22——零第二判据)。
167
+ const slotPlan = resolveProviderPlan({
168
+ providers: agent.providers,
169
+ defaultModel: agent.config?.defaultModel,
170
+ slot: { provider: data.activeProvider ?? null, model: data.activeModel ?? null },
171
+ })
172
+ if (slotPlan.source === "slot") {
173
+ const slotProvider = agent.providers?.find((pr) => pr.name === slotPlan.channel)
174
+ // ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空。
175
+ // 模型面序(#841):槽模型 → `defaultModel` 属本渠道 ⇒ 其模型段 → 渠道默认单值
176
+ // (`providers[].model`);三档皆无 ⇒ `null`(不落 model 键——下游沿既有「model 缺失」处置)。
177
+ // F-2d(MODEL-400-FIX)径保留:空串 / 非串槽模型同样兜(#638 非串 = 未登记)——slotPlan.model
178
+ // 已按同归一出值(单源,无第二判据)。
168
179
  const prevName = agent.activeProvider
169
180
  const prevModel = agent.activeModel ?? null
170
- // #638(adoption 边界归一):非串 `data.activeModel`(外端脏载 / 手工档)= 未登记 ⇒ 回退渠道
171
- // 模型——类型脏值不入两状态字段(镜下方阈值重算门 ∕ 档位块 ∕ VSC `panel-turn-stages.mjs:53` 同形门)。
172
- const slotModel = (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : slotProvider.model
181
+ const slotModel = slotPlan.model
173
182
  const switched = prevName !== slotProvider.name || prevModel !== slotModel
174
183
  agent.activeProvider = slotProvider.name
175
184
  agent.activeModel = slotModel
@@ -200,8 +209,8 @@ export function applySession(agent, data, opts = {}) {
200
209
  }
201
210
  return switched
202
211
  }
203
- // ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
204
- // config/defaultModel 运行时;两方都无效由调用侧复验 validateProvider 弹重选)
212
+ // ② 槽无 key ∥ 不在册 → D-S3 保留——静默保持现状(落 config 链:config 统一解析产物已是
213
+ // `agent.provider`;不报错不纠正——两方都无效由调用侧复验 validateProvider 弹重选)
205
214
  return false
206
215
  }
207
216