@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/session-lifecycle.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { existsSync } from "node:fs"
|
|
21
21
|
import { resolveCompactThreshold } from "./config.mjs"
|
|
22
22
|
// 槽面判据单源(#841 KD-841-2:槽渠道 key 门 + 模型面序——PROVIDER.md §6.22)。
|
|
23
|
-
import { resolveProviderPlan } from "./model-ref.mjs"
|
|
23
|
+
import { hasKey, resolveChannelModel, resolveProviderPlan } from "./model-ref.mjs"
|
|
24
24
|
import {
|
|
25
25
|
slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile, getSessionId,
|
|
26
26
|
writeEndMarker, sessionEnd, ownerPid, ownerPids,
|
|
@@ -77,10 +77,10 @@ function stripTruncatedToolArgs(m) {
|
|
|
77
77
|
*
|
|
78
78
|
* MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
|
|
79
79
|
* "三支"误读):
|
|
80
|
-
* ① 槽 provider 存在 → provider/model
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* compactThreshold(auto
|
|
80
|
+
* ① 槽 provider 存在 → provider/model 按槽值设(模型链 = 槽模型 → `defaultModel` 属本渠道 ⇒ 其模型段;
|
|
81
|
+
* 两档皆无 ⇒ `null`——2026-10-09 清除批:原 `providers[].model` 回落退场;`defaultModel` 经
|
|
82
|
+
* `resolveProviderPlan` 参与模型面)+ 重算
|
|
83
|
+
* compactThreshold(auto 时——按恢复后模型);
|
|
84
84
|
* ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
|
|
85
85
|
* 复验 validateProvider 弹重选)。
|
|
86
86
|
* 删旧支:"activeModel==null 清 stale override 回渠道默认"——前提 = 渠道默认字段——已随
|
|
@@ -172,8 +172,8 @@ export function applySession(agent, data, opts = {}) {
|
|
|
172
172
|
if (slotPlan.source === "slot") {
|
|
173
173
|
const slotProvider = agent.providers?.find((pr) => pr.name === slotPlan.channel)
|
|
174
174
|
// ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空。
|
|
175
|
-
// 模型面序(#841):槽模型 → `defaultModel` 属本渠道 ⇒
|
|
176
|
-
//
|
|
175
|
+
// 模型面序(#841):槽模型 → `defaultModel` 属本渠道 ⇒ 其模型段;两档皆无 ⇒ `null`(不落
|
|
176
|
+
// model 键——下游沿既有「model 缺失」处置;渠道单值模型退场——2026-10-09 清除批)。
|
|
177
177
|
// F-2d(MODEL-400-FIX)径保留:空串 / 非串槽模型同样兜(#638 非串 = 未登记)——slotPlan.model
|
|
178
178
|
// 已按同归一出值(单源,无第二判据)。
|
|
179
179
|
const prevName = agent.activeProvider
|
|
@@ -379,21 +379,25 @@ export function slotOccupancy(cwd, slot) {
|
|
|
379
379
|
* 读数百分数(整数)。与 `applySession` 后读数**同源同式**(四事同判——组合既有件,零新公式):
|
|
380
380
|
* ① 线选 = `contextHistory` 非空取之,否则 `history` 经 `stripTruncatedToolArgs` 回退(同 applySession);
|
|
381
381
|
* ② 回声归并 = `mergeAdjacentAssistantEchoes`(同 applySession 装线前一步);
|
|
382
|
-
* ③ 渠道合并 = `providers` 命中 `data.activeProvider` ⇒
|
|
383
|
-
*
|
|
382
|
+
* ③ 渠道合并 = `providers` 命中 `data.activeProvider` ⇒ 槽模型(串值 `activeModel`;非串 ∕ 空串 = 未登记
|
|
383
|
+
* ⇒ 落链)∥ 无槽模型 ⇒ 现行模型链回落(`defaultModel` 属本渠道 ⇒ 其模型段;两档皆无 ⇒ `null`——
|
|
384
|
+
* `resolveChannelModel`,2026-10-09 清除批:渠道单值模型退场,零静默回落);未命中 ⇒ `fallback`
|
|
385
|
+
* (支 ②「静默保持现状」的装配口径 = `loadConfig().provider`);
|
|
384
386
|
* ④ 公式 = `historyPercent`(`thincoder-core/token-window.mjs`——与 CLI 状态行 / VSC 同式)。
|
|
385
|
-
* **零副作用**:`data` / `providers` / `fallback` 皆入参(不读盘、不写盘、不改任何进程态);
|
|
387
|
+
* **零副作用**:`data` / `providers` / `fallback` / `defaultModel` 皆入参(不读盘、不写盘、不改任何进程态);
|
|
386
388
|
* `data` 非对象 ⇒ `0`(与空历史同值——「非正 ⇒ 零节点」显示门归端侧,沿 `historyPercent` 空历史口径)。 */
|
|
387
|
-
export function sessionReading(data, { providers, fallback } = {}) {
|
|
389
|
+
export function sessionReading(data, { providers, fallback, defaultModel } = {}) {
|
|
388
390
|
if (data === null || typeof data !== "object") return 0
|
|
389
391
|
const full = Array.isArray(data.history) ? data.history : []
|
|
390
392
|
const ch = data.contextHistory
|
|
391
393
|
const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
|
|
392
394
|
const line = mergeAdjacentAssistantEchoes(machine)
|
|
393
|
-
const entry = data.activeProvider ? providers?.find((pr) => pr.name === data.activeProvider) : null
|
|
394
|
-
// #638:非串 `activeModel` 归一(与 applySession 模型合并支 ① 同判)——脏值不达 `historyPercent
|
|
395
|
+
const entry = data.activeProvider ? providers?.find((pr) => pr.name === data.activeProvider && hasKey(pr)) : null
|
|
396
|
+
// #638:非串 ∕ 空串 `activeModel` 归一(与 applySession 模型合并支 ① 同判)——脏值不达 `historyPercent`;
|
|
397
|
+
// 未登记 ⇒ 落现行模型链(`defaultModel` 属本渠段 ∥ `null`——渠道单值退场,2026-10-09 清除批)。
|
|
398
|
+
const slotModel = (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : null
|
|
395
399
|
const provider = entry
|
|
396
|
-
? { ...entry, model:
|
|
400
|
+
? { ...entry, model: slotModel ?? resolveChannelModel(entry, defaultModel ?? null) }
|
|
397
401
|
: fallback
|
|
398
402
|
return historyPercent(line, provider)
|
|
399
403
|
}
|
package/session-slot-claims.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `unknown`(探测失败 / 缺行)⇒ 不认领 / 不判死 / 不删(D-MI10)。
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync } from "node:fs"
|
|
19
|
-
// 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec
|
|
19
|
+
// 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec——判据与探测面分离)。
|
|
20
20
|
import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.mjs"
|
|
21
21
|
// 存储原语(路径 / 原子写 / 进程 sessionId)——静态环见头注。
|
|
22
22
|
import { getSessionId, manifestPath, slotPath } from "./session-slots.mjs"
|
|
@@ -210,7 +210,7 @@ export const _releaseStats = { calls: 0 }
|
|
|
210
210
|
* 早退面:磁盘无 manifest ⇒ `existsSync` 单条目早退**零写**(不造盘面);本进程无认领 ⇒
|
|
211
211
|
* 零写返回 false。失败容忍(F-XR1「退出恒达」):整体 try/catch **永不抛出**,返回 boolean
|
|
212
212
|
* (true = 有释放且落盘成功;false = 早退 / 失败 / **写面拒写**——`saveManifest` 返回值透传,
|
|
213
|
-
* §6.23 判据句 3
|
|
213
|
+
* §6.23 判据句 3——断言);写失败盘面 = 认领残留 →
|
|
214
214
|
* 恢复走既有探测面(现状形态,数据零险)。 */
|
|
215
215
|
export function releaseClaimsAll(cwd) {
|
|
216
216
|
_releaseStats.calls += 1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-slots-manifest.mjs — 清单 / 认领 / 属主面(init-block 批 · F-MI7 拆分:
|
|
3
3
|
* `session-slots.mjs` 探测束接线后实读 517 行,破 490 判定线并越 500 硬限 ⇒ 按
|
|
4
|
-
* `CORE-UNIFICATION.md` §2.8.1
|
|
4
|
+
* `CORE-UNIFICATION.md` §2.8.1 子表原行 3(已随 2026-10-08 批清账)随批执行外提;各函数语义**原样**迁移):
|
|
5
5
|
* 摘要面(`extractSlotMeta` / `slotDigest`)· 清单读写(`loadManifest` / `saveManifest`)·
|
|
6
6
|
* 属主面(`ownerPid` / `ownerPids` / `ownerStateOf` / `ownerArgs` / `cleanDeadOwners`)·
|
|
7
7
|
* 认领面(`ensureActive` / `allocateFresh` / `claimSlot` / `activeSlot` / **认领释放集
|
package/session-slots.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* re-export 全部导出以保持既有 import 兼容)。
|
|
5
5
|
*
|
|
6
6
|
* init-block 批 · F-MI7 二次拆分:探测束接线后本档实读 517 行(破 490 判定线并越 500 硬限)
|
|
7
|
-
* ⇒ 按 `CORE-UNIFICATION.md` §2.8.1
|
|
7
|
+
* ⇒ 按 `CORE-UNIFICATION.md` §2.8.1 子表原行 3(已随 2026-10-08 批清账)**随批**外提清单 / 认领 / 属主面至
|
|
8
8
|
* `session-slots-manifest.mjs`(语义原样迁移;本档 re-export 该档导出保既有 import 面——
|
|
9
9
|
* 消费档 import 路径与名面不动)。本档保留 = 存储原语(cwd 哈希 / 路径 / 原子写 / 进程
|
|
10
10
|
* sessionId)· 端记录(end marker)· 列表面(listSlots——条目集 = 盘面实读;扫描面住
|
|
@@ -33,14 +33,14 @@ import { join, dirname, basename } from "node:path"
|
|
|
33
33
|
import { configDir } from "./config.mjs"
|
|
34
34
|
import { migrateHashLength } from "./session-migrate.mjs"
|
|
35
35
|
// §6.10 D-2(2026-09-05):resumeSlot 决策归本文件(slot/claim 层);data 层读(loadSlotFile/
|
|
36
|
-
// legacy 过滤)属 session.mjs(
|
|
36
|
+
// legacy 过滤)属 session.mjs(800 行内不迁移)——静态环(本档 ↔ session.mjs;init-block 批起
|
|
37
37
|
// 另加本档 ↔ session-slots-manifest.mjs,见头注与 L38-47):函数声明实例化期已初始化、只函数
|
|
38
38
|
// 体内运行时使用(环安全);VS Code 端 session-slots ↔ session-io 同构镜像。
|
|
39
39
|
import { loadSlotFile, isLegacyTransient } from "./session.mjs"
|
|
40
40
|
// TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14 生命周期联动):删槽联动记录存储(store 零项目内依赖——无环)。
|
|
41
41
|
import { unlinkRecordStore } from "./session-store.mjs"
|
|
42
42
|
// 探测束(init-block 批 · F-MI7):实现 / 判据单源住 process-probe.mjs——本档零自有 exec、
|
|
43
|
-
// 零逐 pid
|
|
43
|
+
// 零逐 pid 探测(判据与探测面分离);单 pid 判活 re-export 见下方「判活兼容面」。
|
|
44
44
|
import { probeOwnersAsync, isProcessAlive } from "./process-probe.mjs"
|
|
45
45
|
// 清单 / 认领 / 属主面(init-block 批外提——双向静态环见头注):本档 import 取用 + re-export
|
|
46
46
|
// 保既有 import 面(消费档路径与名面不动)。
|
package/team-ledger.mjs
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* team-ledger.mjs — 团队台账核工具 `team_ledger`(端侧面单源;`docs/core/design/TEAM-LEDGER.md` §2——三端同 import,
|
|
3
|
+
* 2026-10-13 team-ledger 批)。
|
|
4
|
+
*
|
|
5
|
+
* 面:① 工具声明(五 action——depth-0 全量 ∥ 子代理只读变体;两变体同名单对象、schema 窄化;写三 action 于
|
|
6
|
+
* 只读变体 schema 级不可达——D-TL5);② 仓标记派生(会话项目根 ⇒ `git rev-parse --show-toplevel` ⇒
|
|
7
|
+
* `git config --get remote.origin.url` ⇒ **原文**上送 ⇒ 服务端归一——KD-SV-144/D-TL3;端侧零归一实现);
|
|
8
|
+
* ③ 登录态门(`teamStatus().loggedIn` 判据单源——D-TL4;未登录 ⇒ 明示 + **零网络调用**);④ 传输(直接
|
|
9
|
+
* `fetch` + Bearer `team.token` + 超时——沿 `team.mjs` 现形;零第三方 ∥ 零持久化 ∥ 零缓存回读——需求 ③④)。
|
|
10
|
+
*
|
|
11
|
+
* 接线(`agent/family-tools.mjs` 动态 import——沿 `ledgerTool`/`ledgerReadTool` 同位;`W8 契约②` 同律)。
|
|
12
|
+
* 零 import 本机台账模块(KD-SV-145/D-TL2——上送 = 本工具 + 本机 ledger 两步显式动作,零跨账代码路径)。
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from "node:child_process"
|
|
15
|
+
import { loadConfig } from "./config.mjs"
|
|
16
|
+
import { teamStatus } from "./team.mjs"
|
|
17
|
+
import { DESC } from "./tools/shared.mjs"
|
|
18
|
+
|
|
19
|
+
/** 请求超时(同 `team.mjs` `TEAM_FETCH_TIMEOUT_MS` 现值——该常量未导出(`team.mjs` ±0 面),两处同值即口径)。 */
|
|
20
|
+
const TEAM_FETCH_TIMEOUT_MS = 15_000
|
|
21
|
+
|
|
22
|
+
/** action 枚举(全量 / 只读两变体;顺序 = 文档与按钮序)。 */
|
|
23
|
+
const FULL_ACTIONS = ["query", "count", "add", "update", "close"]
|
|
24
|
+
const READ_ACTIONS = ["query", "count"]
|
|
25
|
+
|
|
26
|
+
/** 六态(服务端 CHECK 同集——ledger/LEDGER.md §3)。 */
|
|
27
|
+
const STATUSES = ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"]
|
|
28
|
+
|
|
29
|
+
/** 工具 cwd 供值面(缺省 → 当前项目根)。 */
|
|
30
|
+
const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
|
|
31
|
+
|
|
32
|
+
// ── 仓标记派生(§2.2) ───────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** git 读取面(缺省 = 真 git;失败 ⇒ `null`——allowFail 口径)。 */
|
|
35
|
+
function realGitRun(cwd, args) {
|
|
36
|
+
try {
|
|
37
|
+
return String(execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })).trim()
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let gitRun = realGitRun
|
|
44
|
+
|
|
45
|
+
/** 测试注入:假 git 面(缺省 / 传非函数 ⇒ 复位真 git)。 */
|
|
46
|
+
export function _setGitRunnerForTest(fn) {
|
|
47
|
+
gitRun = typeof fn === "function" ? fn : realGitRun
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 仓标记派生(§2.2 三步):会话项目根 ⇒ `rev-parse --show-toplevel` ⇒ `config --get remote.origin.url`;
|
|
51
|
+
* 无 git 仓 ∥ 无 origin ⇒ `null`(**原文**返回——归一在服务端)。出 = origin 原文 ∥ null。 */
|
|
52
|
+
export function deriveRepo(cwd) {
|
|
53
|
+
const root = gitRun(cwd, ["rev-parse", "--show-toplevel"])
|
|
54
|
+
if (root === null || root === "") return null
|
|
55
|
+
const url = gitRun(root, ["config", "--get", "remote.origin.url"])
|
|
56
|
+
if (url === null || url === "") return null
|
|
57
|
+
return url
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 派生出词(无仓 / 无 origin ⇒ 明示失败——判于网络前,零请求零写库)。 */
|
|
61
|
+
function deriveRepoOrFail(cwd) {
|
|
62
|
+
const repo = deriveRepo(cwd)
|
|
63
|
+
if (repo === null) throw new Error("未检出 git 源(origin)——团队账按 git 源记账")
|
|
64
|
+
return repo
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── 传输(§2.4——沿 teamVerify 现形) ────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/** 请求:`<server>/api/client/ledger*` + `Authorization: Bearer <team.token>` + 超时。出 = JSON 体。
|
|
70
|
+
* 401 ⇒ 「登录已失效」(端侧零清盘——沿 `teamVerify` 三值口径)∥ 网络失败 ⇒ 「服务器不可达」(零缓存回读)
|
|
71
|
+
* ∥ 其余非 2xx ⇒ 服务端人话透传(400/403/404——错误形零新码)。 */
|
|
72
|
+
async function request(server, token, method, path, body = null) {
|
|
73
|
+
let res
|
|
74
|
+
try {
|
|
75
|
+
res = await fetch(`${server}${path}`, {
|
|
76
|
+
method,
|
|
77
|
+
headers: { authorization: `Bearer ${token}`, ...(body === null ? {} : { "content-type": "application/json" }) },
|
|
78
|
+
...(body === null ? {} : { body: JSON.stringify(body) }),
|
|
79
|
+
signal: AbortSignal.timeout(TEAM_FETCH_TIMEOUT_MS),
|
|
80
|
+
})
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error("服务器不可达——团队账不可用(零离线缓存)")
|
|
83
|
+
}
|
|
84
|
+
const text = await res.text().catch(() => "")
|
|
85
|
+
if (res.status === 401) throw new Error("登录已失效——请重新登录")
|
|
86
|
+
let payload = null
|
|
87
|
+
try {
|
|
88
|
+
payload = JSON.parse(text)
|
|
89
|
+
} catch {
|
|
90
|
+
payload = null
|
|
91
|
+
}
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const message = payload?.error?.message
|
|
94
|
+
throw new Error(typeof message === "string" && message !== "" ? message : `团队账请求失败(HTTP ${res.status})`)
|
|
95
|
+
}
|
|
96
|
+
if (payload === null || typeof payload !== "object") throw new Error("团队账响应形非法(非 JSON 对象)——服务不符合预期面")
|
|
97
|
+
return payload
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 工具层参数守卫(沿 `ledger` 工具面先例——声明派生;判于网络前,非法 ⇒ 零请求) ──
|
|
101
|
+
|
|
102
|
+
/** 值预览(文案的 `<预览>`):`JSON.stringify`,超 80 字符截断加 `…`。 */
|
|
103
|
+
const argPreview = (v) => {
|
|
104
|
+
const s = JSON.stringify(v) ?? String(v)
|
|
105
|
+
return s.length > 80 ? `${s.slice(0, 80)}…` : s
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 枚举取值域文本。 */
|
|
109
|
+
const enumText = (values) => `{${values.join(", ")}}`
|
|
110
|
+
|
|
111
|
+
/** 参数守卫(P1–P5:入参形 → 未知键 → 缺失(必填)→ 枚举 / 类型——只判不改)。 */
|
|
112
|
+
function assertArgs(name, decl, args) {
|
|
113
|
+
const props = decl?.properties ?? {}
|
|
114
|
+
const fields = Object.keys(props)
|
|
115
|
+
if (args == null) args = {}
|
|
116
|
+
else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`${name}:参数须为对象(收到 ${argPreview(args)})`)
|
|
117
|
+
const unknown = Object.keys(args).filter((key) => !Object.hasOwn(props, key))
|
|
118
|
+
if (unknown.length > 0) throw new Error(`${name}:未知参数:${unknown.join(" / ")}(可用参数 = ${fields.join(" / ")})`)
|
|
119
|
+
for (const field of fields) {
|
|
120
|
+
const spec = props[field]
|
|
121
|
+
const value = args[field]
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
if ((decl.required ?? []).includes(field)) {
|
|
124
|
+
const suffix = spec.enum ? `;取值 ∈ ${enumText(spec.enum)}` : spec.type === "string" ? ";非空字符串" : ""
|
|
125
|
+
throw new Error(`${name}:${field} 缺失(必填${suffix})`)
|
|
126
|
+
}
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (spec.enum) {
|
|
130
|
+
if (!spec.enum.includes(value)) throw new Error(`${name}:${field} 非法:${argPreview(value)}(取值 ∈ ${enumText(spec.enum)})`)
|
|
131
|
+
} else if (spec.type === "string" && typeof value !== "string") {
|
|
132
|
+
throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为字符串)`)
|
|
133
|
+
} else if (spec.type === "number" && typeof value !== "number") {
|
|
134
|
+
throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为数字)`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return args
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 各 action 声明条目(= 参数并集按 action 收窄的单源;schema 与守卫同源)。 */
|
|
141
|
+
const SCOPE_DESC = `"repo"(缺省——当前仓,自动派生)∥ "team"(全队跨仓)`
|
|
142
|
+
const ACTION_SPECS = {
|
|
143
|
+
query: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
scope: { type: "string", enum: ["repo", "team"], description: SCOPE_DESC },
|
|
147
|
+
status: { type: "string", enum: STATUSES, description: "按状态过滤(六态之一,缺省 = 不过滤)" },
|
|
148
|
+
owner: { type: "string", description: "按负责人过滤(用户名精确匹配)" },
|
|
149
|
+
},
|
|
150
|
+
additionalProperties: false,
|
|
151
|
+
},
|
|
152
|
+
count: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
scope: { type: "string", enum: ["repo", "team"], description: SCOPE_DESC },
|
|
156
|
+
status: { type: "string", enum: STATUSES, description: "按状态过滤(六态之一,缺省 = 不过滤)" },
|
|
157
|
+
owner: { type: "string", description: "按负责人过滤(用户名精确匹配)" },
|
|
158
|
+
},
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
},
|
|
161
|
+
add: {
|
|
162
|
+
type: "object",
|
|
163
|
+
required: ["title"],
|
|
164
|
+
properties: {
|
|
165
|
+
title: { type: "string", description: "标题(必填——trim 非空 ∥ ≤200)" },
|
|
166
|
+
detail: { type: "string", description: "正文(可空 ∥ ≤4000)" },
|
|
167
|
+
owner: { type: "string", description: "负责人(缺省 = 自己)" },
|
|
168
|
+
},
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
},
|
|
171
|
+
update: {
|
|
172
|
+
type: "object",
|
|
173
|
+
required: ["id"],
|
|
174
|
+
properties: {
|
|
175
|
+
id: { type: "number", description: "条目编号(全局)" },
|
|
176
|
+
title: { type: "string", description: "标题(缺省 = 不变)" },
|
|
177
|
+
detail: { type: "string", description: "正文(缺省 = 不变;追认核销前须非空)" },
|
|
178
|
+
owner: { type: "string", description: "负责人(交接 = 换负责人;缺省 = 不变)" },
|
|
179
|
+
status: { type: "string", enum: STATUSES, description: "目标状态(迁移表合法后继——非法由服务端 400)" },
|
|
180
|
+
},
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
},
|
|
183
|
+
close: {
|
|
184
|
+
type: "object",
|
|
185
|
+
required: ["id", "status"],
|
|
186
|
+
properties: {
|
|
187
|
+
id: { type: "number", description: "条目编号(全局)" },
|
|
188
|
+
status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(收口 / 撤回)" },
|
|
189
|
+
},
|
|
190
|
+
additionalProperties: false,
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 并集 schema(`action` 枚举 + 五 action 参数并集;per-action 必填 / 收窄由运行时守卫裁)。 */
|
|
195
|
+
const UNION_PARAMETERS = {
|
|
196
|
+
type: "object",
|
|
197
|
+
required: ["action"],
|
|
198
|
+
properties: {
|
|
199
|
+
action: { type: "string", enum: FULL_ACTIONS, description: "" }, // 变体构造时填描述
|
|
200
|
+
scope: { type: "string", enum: ["repo", "team"], description: SCOPE_DESC },
|
|
201
|
+
status: { type: "string", enum: STATUSES, description: "状态(query ∥ count 过滤——六态之一;update 目标态——迁移表合法后继;close ∈ {已核销, 已废弃})" },
|
|
202
|
+
owner: { type: "string", description: "负责人(query ∥ count 过滤——用户名精确匹配;add ∥ update 设值)" },
|
|
203
|
+
title: { type: "string", description: "标题(add 必填 ∥ update 可改——trim 非空 ∥ ≤200)" },
|
|
204
|
+
detail: { type: "string", description: "正文(add ∥ update——可空 ∥ ≤4000;追认核销须非空)" },
|
|
205
|
+
id: { type: "number", description: "条目编号(update ∥ close 必填——全局编号)" },
|
|
206
|
+
},
|
|
207
|
+
additionalProperties: false,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 变体构造(同名单对象、schema 窄化、两形永不同装配——先例 = `ledger` 工具 depth 面变体)。 */
|
|
211
|
+
function makeTeamLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
|
|
212
|
+
return {
|
|
213
|
+
name: "team_ledger",
|
|
214
|
+
description: DESC("team_ledger"), // 文本单点 = tool-docs/team_ledger.md(DESC 加载面)
|
|
215
|
+
parameters: {
|
|
216
|
+
...UNION_PARAMETERS,
|
|
217
|
+
properties: {
|
|
218
|
+
...UNION_PARAMETERS.properties,
|
|
219
|
+
action: {
|
|
220
|
+
...UNION_PARAMETERS.properties.action,
|
|
221
|
+
enum: actions,
|
|
222
|
+
description: writeFace ? "操作(五值):query 查询 / count 计数 / add 新增 / update 更新 / close 收口或撤回——写三仅主 agent 装配面" : "操作(只读二值):query 查询 / count 计数",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
readonly,
|
|
227
|
+
isReadonlyAction(args) { return READ_ACTIONS.includes(args?.action) },
|
|
228
|
+
async execute(args, ctx) {
|
|
229
|
+
if (args == null) args = {}
|
|
230
|
+
else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`team_ledger:参数须为对象(收到 ${argPreview(args)})`)
|
|
231
|
+
const { action, ...rest0 } = args
|
|
232
|
+
assertArgs("team_ledger", { properties: { action: { type: "string", enum: actions } }, required: ["action"] }, { action })
|
|
233
|
+
const rest = assertArgs(`team_ledger(${action})`, ACTION_SPECS[action], rest0)
|
|
234
|
+
// 登录态门(D-TL4——判据单源 = teamStatus().loggedIn;未登录 ⇒ 明示 + 零网络调用)
|
|
235
|
+
const status = teamStatus()
|
|
236
|
+
if (!status.loggedIn || status.server === null) throw new Error("未登录——团队账不可用(本机台账照常);请先登录")
|
|
237
|
+
const token = loadConfig().team?.token // token 取值面(状态读面不下发 token——TEAM.md §2.1)
|
|
238
|
+
const server = status.server
|
|
239
|
+
const cwd = cwdOf(ctx)
|
|
240
|
+
switch (action) {
|
|
241
|
+
case "query": {
|
|
242
|
+
const payload = await request(server, token, "GET", `/api/client/ledger${queryOf(rest, cwd)}`)
|
|
243
|
+
return JSON.stringify(payload, null, 2)
|
|
244
|
+
}
|
|
245
|
+
case "count": {
|
|
246
|
+
const payload = await request(server, token, "GET", `/api/client/ledger${queryOf(rest, cwd)}`)
|
|
247
|
+
return JSON.stringify({ count: payload.total })
|
|
248
|
+
}
|
|
249
|
+
case "add": {
|
|
250
|
+
const repo = deriveRepoOrFail(cwd)
|
|
251
|
+
const payload = await request(server, token, "POST", "/api/client/ledger", { repo, title: rest.title, detail: rest.detail, owner: rest.owner })
|
|
252
|
+
return JSON.stringify(payload, null, 2)
|
|
253
|
+
}
|
|
254
|
+
case "update": {
|
|
255
|
+
const payload = await request(server, token, "PATCH", `/api/client/ledger/${encodeURIComponent(String(rest.id))}`, { title: rest.title, detail: rest.detail, owner: rest.owner, status: rest.status })
|
|
256
|
+
return JSON.stringify(payload, null, 2)
|
|
257
|
+
}
|
|
258
|
+
case "close": {
|
|
259
|
+
const payload = await request(server, token, "POST", `/api/client/ledger/${encodeURIComponent(String(rest.id))}/close`, { status: rest.status })
|
|
260
|
+
return JSON.stringify(payload, null, 2)
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
// 不可达(入口枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
|
|
264
|
+
throw new Error(`team_ledger:action 非法:${argPreview(action)}(取值 ∈ ${enumText(actions)})`)
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 查询串装配(§2.1):`scope = "repo"`(缺省)⇒ 派生当前仓(无源 ⇒ 明示失败)∥ `"team"` ⇒ 不带 `repo`。 */
|
|
271
|
+
function queryOf(rest, cwd) {
|
|
272
|
+
const params = new URLSearchParams()
|
|
273
|
+
if ((rest.scope ?? "repo") === "repo") params.set("repo", deriveRepoOrFail(cwd))
|
|
274
|
+
if (rest.status != null) params.set("status", rest.status)
|
|
275
|
+
if (rest.owner != null) params.set("owner", rest.owner)
|
|
276
|
+
const text = params.toString()
|
|
277
|
+
return text === "" ? "" : `?${text}`
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 统一入口两变体(同名 `team_ledger`——depth-0 装配全量 ∥ depth>0 各角色段装配只读)。 */
|
|
281
|
+
export const teamLedgerTool = makeTeamLedgerTool(FULL_ACTIONS, { writeFace: true })
|
|
282
|
+
export const teamLedgerReadTool = makeTeamLedgerTool(READ_ACTIONS, { readonly: true })
|
package/team.mjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* team.mjs — 团队登录机制(TEAM.md §2——三端共用单源:CLI ∥ VSC ∥ 桌面同 import)。
|
|
3
|
+
*
|
|
4
|
+
* 面:登录 ∥ 退出 ∥ 登录态读面 ∥ 活校验(`teamVerify`——只读三值,登录面补全批增)∥ 端标签(自动生成)∥
|
|
5
|
+
* 派生 provider 条目(登录写入 / 退出停用)∥ 服务地址归一 ∥ 失败分类。**写面单源** = 本档(`writeConfigAtomic`
|
|
6
|
+
* 一次 mutate——经 `persistRaw`;端侧零自写盘,三端同源)——`teamVerify` **只读**(零写盘、零状态)。
|
|
7
|
+
*
|
|
8
|
+
* 形(TEAM.md §2.1):顶层 `team` 段 `{ server, member{username,name}, label, token }`——`token` 在场 ⇔
|
|
9
|
+
* 已登录(权威源;不入 `DEFAULTS`);派生条目 `{ name:"team", baseURL:"<server>/v1", apiKey:"<token>",
|
|
10
|
+
* derived:true }` —— 登录 upsert(有则更新 ∥ 无则追加 `providers[]` 表尾——不劫持既有回退序);退出 =
|
|
11
|
+
* 摘 `token` ∥ 摘 `apiKey`(条目与 `derived` 保留——重登自动恢复)。同名手工条目 ⇒ 登录成但不覆盖
|
|
12
|
+
* (D-TM4——返回提示码)。
|
|
13
|
+
*
|
|
14
|
+
* 返回形(B1 批端侧契约——父侧裁定 2026-10-10):
|
|
15
|
+
* `teamLogin` ⇒ `{ ok:true, notice?:"manual-name-conflict" }` ∥ `{ ok:false, reason }`
|
|
16
|
+
* `teamLogout` ⇒ `{ ok:true, revokeDelivered:boolean }` ∥ `{ ok:false, reason }`
|
|
17
|
+
* `reason` 取值域:`network`(不可达 ∥ DNS/TLS ∥ 超时 ∥ 非 401/429 应答——服务不符合预期面)∥
|
|
18
|
+
* `credentials`(401——同措辞)∥ `rate_limited`(429——锁定期)∥ `write_failed`(服务端已应答、本地
|
|
19
|
+
* 写盘失败——mtime 冲突 ∥ 畸形档拒写)。`notice` 携**码**不携文(文本归端侧 i18n);
|
|
20
|
+
* `revokeDelivered:false` = 服务端吊销未达(网络失败——本地照清)。
|
|
21
|
+
*/
|
|
22
|
+
import { hostname } from "node:os"
|
|
23
|
+
|
|
24
|
+
import { loadConfig } from "./config.mjs"
|
|
25
|
+
import { persistRaw } from "./config-io.mjs"
|
|
26
|
+
import { sessionEnd } from "./session-slots.mjs"
|
|
27
|
+
|
|
28
|
+
/** 派生 provider 条目固定名(TEAM.md §2.1 ∥ PROVIDER.md §6.25)。 */
|
|
29
|
+
const TEAM_PROVIDER_NAME = "team"
|
|
30
|
+
|
|
31
|
+
/** 端标签总长上限(= key 名称上限——accounts/ACCOUNTS.md §1.1;自动生成超长 ⇒ 裁剪)。 */
|
|
32
|
+
const LABEL_MAX = 40
|
|
33
|
+
|
|
34
|
+
/** 登录/退出请求超时(毫秒)——超时归 `network` 分类(防悬挂)。 */
|
|
35
|
+
const TEAM_FETCH_TIMEOUT_MS = 15_000
|
|
36
|
+
|
|
37
|
+
/** 登录提示码:同名手工 provider 存在 ⇒ 派生条目未写(不覆盖——D-TM4;端侧就地提示)。 */
|
|
38
|
+
export const NOTICE_MANUAL_NAME_CONFLICT = "manual-name-conflict"
|
|
39
|
+
|
|
40
|
+
/** 写入口根形守卫:`writeConfigAtomic` 不校验根形——数组 ∥ 非对象根会让 mutate 落空(`JSON.stringify` 丢弃
|
|
41
|
+
* 非索引属性——写回原文)⇒ 假「已写」;本档 0 值 = 不产出假成功 ⇒ 拒写(上层归一 `write_failed`)。 */
|
|
42
|
+
function assertWritableRoot(raw) {
|
|
43
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("config 根非对象——拒写")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 服务地址归一(TEAM.md §2.1 ∥ 用例 E2):去首尾空白;缺协议补 `http://`;去尾斜杠;尾 `/v1` 去。 */
|
|
47
|
+
export function normalizeTeamServer(server) {
|
|
48
|
+
let text = typeof server === "string" ? server.trim() : ""
|
|
49
|
+
if (text === "") return null
|
|
50
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) text = `http://${text}` // 缺协议补 http://
|
|
51
|
+
text = text.replace(/\/+$/, "") // 去尾斜杠
|
|
52
|
+
text = text.replace(/\/v1$/i, "") // 尾 `/v1` 去(粘贴 OpenAI 形基址)
|
|
53
|
+
text = text.replace(/\/+$/, "")
|
|
54
|
+
return /^[a-z][a-z0-9+.-]*:\/\/.+/i.test(text) ? text : null // 纯协议(空 host)⇒ 无效
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 端标签自动生成(D-TM5):`端名@主机名`(端名 = 进程端名缝——`sessionEnd()`,cli/vscode/desktop);
|
|
58
|
+
* 总长 ≤40 裁剪。零输入字段(表单无标签项)。 */
|
|
59
|
+
export function defaultTeamLabel() {
|
|
60
|
+
let host = ""
|
|
61
|
+
try {
|
|
62
|
+
host = hostname() // os.hostname 失败面兜底(不阻登录)
|
|
63
|
+
} catch {
|
|
64
|
+
host = ""
|
|
65
|
+
}
|
|
66
|
+
return `${sessionEnd()}@${host}`.slice(0, LABEL_MAX)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 登录态读面(桌面 `team:status` ∥ VSC `teamStatus` 投影源):`loggedIn` ⇔ `team.token` 在场。 */
|
|
70
|
+
export function teamStatus() {
|
|
71
|
+
const team = loadConfig().team
|
|
72
|
+
return {
|
|
73
|
+
loggedIn: Boolean(team?.token),
|
|
74
|
+
server: team?.server ?? null,
|
|
75
|
+
member: team?.member ?? null,
|
|
76
|
+
label: team?.label ?? null,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 登录态**活校验**(TEAM.md §2.6 ∥ 登录面补全批 · 2026-10-10 · 台账 #1231——本批新增):`GET <server>/api/client/me`
|
|
82
|
+
* (Bearer = `team.token`)⇒ **三值闭集**:`{ state: "valid" }`(200——token 有效)∥ `{ state: "invalid" }`(401——
|
|
83
|
+
* 被吊销 ∥ 无效)∥ `{ state: "unreachable" }`(网络不可达 ∥ 其他非 401 失败——**不判失效**:离线容忍)。
|
|
84
|
+
* **只读**——零写盘、零状态(调用面持结果);零周期轮询(触发点制 = 启动一次 ∥ 登/退面开合 ∥ CLI `/team status`)。
|
|
85
|
+
* token ∥ 地址缺席(未登录 ∥ 畸形档)⇒ `unreachable`(无从校验——不假报失效;端侧未登录面零调本函数)。
|
|
86
|
+
*/
|
|
87
|
+
export async function teamVerify() {
|
|
88
|
+
const team = loadConfig().team
|
|
89
|
+
const token = typeof team?.token === "string" && team.token !== "" ? team.token : null
|
|
90
|
+
const server = typeof team?.server === "string" && team.server !== "" ? team.server : null
|
|
91
|
+
if (token === null || server === null) return { state: "unreachable" }
|
|
92
|
+
let res
|
|
93
|
+
try {
|
|
94
|
+
res = await fetch(`${server}/api/client/me`, {
|
|
95
|
+
headers: { authorization: `Bearer ${token}` },
|
|
96
|
+
signal: AbortSignal.timeout(TEAM_FETCH_TIMEOUT_MS),
|
|
97
|
+
})
|
|
98
|
+
} catch {
|
|
99
|
+
return { state: "unreachable" } // 不可达 ∥ DNS ∥ TLS ∥ 超时
|
|
100
|
+
}
|
|
101
|
+
await res.text().catch(() => {}) // 回执体零消费面(三值只看状态)
|
|
102
|
+
if (res.status === 200) return { state: "valid" }
|
|
103
|
+
if (res.status === 401) return { state: "invalid" }
|
|
104
|
+
return { state: "unreachable" } // 其余(403 ∥ 404 地址非本服务 ∥ 5xx)⇒ 不判失效
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 登录(TEAM.md §2.2;三端同构):请求 `POST <server>/api/client/login` ⇒ 成 ⇒ **一次写盘**(`team` 段 +
|
|
109
|
+
* 派生条目 upsert);败 ⇒ 分类出词、零写盘。`label` 缺省 ⇒ 自动生成(`defaultTeamLabel()`);显式传入
|
|
110
|
+
* 同口径裁剪 ≤40(服务端 >40 ⇒ 400)。网络面成功但写盘失败 ⇒ `write_failed`(服务端 token 已签发——
|
|
111
|
+
* 可见于 key 列表,本地未落)。
|
|
112
|
+
*/
|
|
113
|
+
export async function teamLogin({ server = null, username = null, password = null, label = null } = {}) {
|
|
114
|
+
const base = normalizeTeamServer(server)
|
|
115
|
+
if (base === null) return { ok: false, reason: "network" } // 无地址可打(端侧表单门兜底面)
|
|
116
|
+
if (typeof username !== "string" || username === "" || typeof password !== "string" || password === "") {
|
|
117
|
+
return { ok: false, reason: "credentials" } // 凭据不全(端侧表单门兜底面)
|
|
118
|
+
}
|
|
119
|
+
const resolvedLabel = (typeof label === "string" && label.trim() !== "" ? label.trim() : defaultTeamLabel()).slice(0, LABEL_MAX) // 端标签 ≤40 裁剪(显式传入同口径——服务端 >40 ⇒ 400)
|
|
120
|
+
let res
|
|
121
|
+
try {
|
|
122
|
+
res = await fetch(`${base}/api/client/login`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: { "content-type": "application/json" },
|
|
125
|
+
body: JSON.stringify({ username, password, label: resolvedLabel }),
|
|
126
|
+
signal: AbortSignal.timeout(TEAM_FETCH_TIMEOUT_MS),
|
|
127
|
+
})
|
|
128
|
+
} catch {
|
|
129
|
+
return { ok: false, reason: "network" } // 不可达 ∥ DNS ∥ TLS ∥ 超时
|
|
130
|
+
}
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
await res.text().catch(() => {}) // 错误体零消费面(分类只看状态——零新码)
|
|
133
|
+
if (res.status === 401) return { ok: false, reason: "credentials" }
|
|
134
|
+
if (res.status === 429) return { ok: false, reason: "rate_limited" }
|
|
135
|
+
return { ok: false, reason: "network" } // 404(地址不是本服务)∥ 5xx 等——服务不符合预期面
|
|
136
|
+
}
|
|
137
|
+
let body = null
|
|
138
|
+
try {
|
|
139
|
+
body = await res.json()
|
|
140
|
+
} catch {
|
|
141
|
+
body = null
|
|
142
|
+
}
|
|
143
|
+
const token = typeof body?.token === "string" && body.token !== "" ? body.token : null
|
|
144
|
+
if (token === null) return { ok: false, reason: "network" } // 200 而形不符(代理 ∥ 非本服务应答)
|
|
145
|
+
const memberInfo = body?.member !== null && typeof body?.member === "object" ? body.member : null
|
|
146
|
+
const memberUsername = typeof memberInfo?.username === "string" && memberInfo.username !== "" ? memberInfo.username : username
|
|
147
|
+
const memberName = typeof memberInfo?.name === "string" && memberInfo.name !== "" ? memberInfo.name : memberUsername
|
|
148
|
+
let notice = null
|
|
149
|
+
try {
|
|
150
|
+
const result = persistRaw((raw) => {
|
|
151
|
+
assertWritableRoot(raw)
|
|
152
|
+
// 一次 mutate:team 段 + 派生条目(同一次写盘落)
|
|
153
|
+
raw.team = { server: base, member: { username: memberUsername, name: memberName }, label: resolvedLabel, token }
|
|
154
|
+
if (!Array.isArray(raw.providers)) raw.providers = []
|
|
155
|
+
const existing = raw.providers.find((p) => p !== null && typeof p === "object" && p.name === TEAM_PROVIDER_NAME)
|
|
156
|
+
if (existing !== undefined && existing.derived !== true) {
|
|
157
|
+
notice = NOTICE_MANUAL_NAME_CONFLICT // 同名手工条目 ⇒ 不覆盖(D-TM4)——登录仍成
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (existing !== undefined) {
|
|
161
|
+
existing.baseURL = `${base}/v1`
|
|
162
|
+
existing.apiKey = token
|
|
163
|
+
existing.derived = true
|
|
164
|
+
} else {
|
|
165
|
+
raw.providers.push({ name: TEAM_PROVIDER_NAME, baseURL: `${base}/v1`, apiKey: token, derived: true }) // 表尾追加
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
if (result !== null && result.ok === false) return { ok: false, reason: "write_failed" } // mtime 冲突(F5b——不自动合并)
|
|
169
|
+
} catch {
|
|
170
|
+
return { ok: false, reason: "write_failed" } // 畸形档拒写等(writeConfigAtomic fail-closed)
|
|
171
|
+
}
|
|
172
|
+
return notice !== null ? { ok: true, notice } : { ok: true }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 退出(TEAM.md §2.3):`POST <server>/api/client/logout`(best-effort——网络失败 ⇒ 仍清本地)⇒ **一次
|
|
177
|
+
* 写盘**:摘 `team.token`(server/member/label 留存)+ 摘派生条目 `apiKey`(条目与 `derived` 保留)。
|
|
178
|
+
* `revokeDelivered:false` = 服务端吊销未达(端侧提示);401 = token 已失效(视同已送达——照清本地)。
|
|
179
|
+
*/
|
|
180
|
+
export async function teamLogout() {
|
|
181
|
+
const config = loadConfig()
|
|
182
|
+
const team = config.team
|
|
183
|
+
const token = team?.token ?? null
|
|
184
|
+
const providers = Array.isArray(config.providers) ? config.providers : []
|
|
185
|
+
const derived = providers.find((p) => p !== null && typeof p === "object" && p.name === TEAM_PROVIDER_NAME && p.derived === true)
|
|
186
|
+
const hasDerivedKey = typeof derived?.apiKey === "string" && derived.apiKey !== ""
|
|
187
|
+
let revokeDelivered = true
|
|
188
|
+
if (token !== null) {
|
|
189
|
+
if (team.server === null) {
|
|
190
|
+
revokeDelivered = false // 有 token 无地址 ⇒ 无法吊销(畸形档面)
|
|
191
|
+
} else {
|
|
192
|
+
try {
|
|
193
|
+
const res = await fetch(`${team.server}/api/client/logout`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
196
|
+
body: "{}",
|
|
197
|
+
signal: AbortSignal.timeout(TEAM_FETCH_TIMEOUT_MS),
|
|
198
|
+
})
|
|
199
|
+
await res.text().catch(() => {})
|
|
200
|
+
revokeDelivered = res.status === 200 || res.status === 401 // 401 = 已失效(端侧视同「已失效」——照清本地)
|
|
201
|
+
} catch {
|
|
202
|
+
revokeDelivered = false // 不可达 ⇒ 本地仍清 + 提示「服务端吊销未达」
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (token !== null || hasDerivedKey) {
|
|
207
|
+
try {
|
|
208
|
+
const result = persistRaw((raw) => {
|
|
209
|
+
assertWritableRoot(raw)
|
|
210
|
+
const section = raw.team
|
|
211
|
+
if (section !== null && typeof section === "object" && !Array.isArray(section)) delete section.token
|
|
212
|
+
if (Array.isArray(raw.providers)) {
|
|
213
|
+
const entry = raw.providers.find((p) => p !== null && typeof p === "object" && p.name === TEAM_PROVIDER_NAME && p.derived === true)
|
|
214
|
+
if (entry !== undefined) delete entry.apiKey // 条目与 derived 标记保留(重登自动恢复)
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
if (result !== null && result.ok === false) return { ok: false, reason: "write_failed" }
|
|
218
|
+
} catch {
|
|
219
|
+
return { ok: false, reason: "write_failed" }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, revokeDelivered }
|
|
223
|
+
}
|
package/tool-docs/bash.md
CHANGED
|
@@ -12,6 +12,7 @@ Parameters:
|
|
|
12
12
|
- command (required): Shell command to execute
|
|
13
13
|
- timeout: Timeout in milliseconds (default 120000, max ~300000)
|
|
14
14
|
- filter: Optional — a regex; only output lines matching it are returned (case-insensitive). Use instead of hand-writing a pipe into `findstr`/`grep`.
|
|
15
|
+
- async: Run in the background (default false) — returns an ack at once (bash#<id>, log path); the result arrives as a digest when the process exits. Depth-0 only.
|
|
15
16
|
{{inject:bash-terminal-face}}
|
|
16
17
|
|
|
17
18
|
Output format:
|