@shgroup/dsh-serenity-hooks 1.48.0 → 1.48.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/dsh.plugin.json +1 -1
- package/lib/git-ops.d.ts +17 -3
- package/lib/index.js +301 -94
- package/lib/opencode-provider.d.ts +15 -1
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.48.
|
|
3
|
+
"version": "1.48.2",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag;后两个按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2(deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
package/lib/git-ops.d.ts
CHANGED
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
* git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
|
|
3
3
|
*
|
|
4
4
|
* 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
|
|
5
|
-
* - status:JSON {clean, files:[{status,file}], summary}
|
|
6
|
-
* - commit
|
|
7
|
-
* - push
|
|
5
|
+
* - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
|
|
6
|
+
* - commit:默认「只提交指定 paths」;未给 paths ⇒ 整仓 add -A(并在回执里显式告知)
|
|
7
|
+
* - commit:**默认随后 push**(`noPush` 可关);push 结果如实附在回执里
|
|
8
|
+
* - push:成功判据 = **`git push --porcelain` 的 stdout 里读到 ref 行**(绝不采信"stderr 为空")
|
|
8
9
|
* - pull:git fetch + merge --ff-only;up-to-date / [REJECTED] 建议
|
|
9
10
|
* - log:--oneline [-n <count>]
|
|
10
11
|
* - diff:git diff [--cached] [<ref>] [-- <path>]
|
|
11
12
|
* 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 2026-09-26 修(tiangong-serenity 报障):**push 曾把"超时被杀"报成成功**。
|
|
15
|
+
* 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
|
|
16
|
+
* `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
|
|
17
|
+
* 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
|
|
18
|
+
* 把两者判成同一件事,回显 `Pushed to origin/<branch>` 而远程 ref 从未变。
|
|
19
|
+
* ⇒ 本版三处硬修:① `git()` 交出**结构化结果**(ok/timedOut/status),守卫一律看它,不再看
|
|
20
|
+
* "stderr 是否为空";② push 的成功判据改为**可核验事实**(`--porcelain` 的 ref 行);
|
|
21
|
+
* ③ 超时/未确认一律**不报成功**,并给出 `git ls-remote` 核验指令。
|
|
12
22
|
*/
|
|
13
23
|
import type { JsonValue } from './json.js';
|
|
14
24
|
type GitAction = 'status' | 'commit' | 'push' | 'log' | 'pull' | 'diff';
|
|
@@ -16,6 +26,10 @@ export declare const GIT_ACTIONS: readonly GitAction[];
|
|
|
16
26
|
interface GitArgs {
|
|
17
27
|
action: GitAction;
|
|
18
28
|
message?: string;
|
|
29
|
+
/** commit:只提交这些路径(不传 ⇒ 整仓 `add -A`) */
|
|
30
|
+
paths?: string[];
|
|
31
|
+
/** commit:只提交到本地、不随后推送(默认提交后自动推送) */
|
|
32
|
+
noPush?: boolean;
|
|
19
33
|
/** log 条数(对齐 osp 参数 n,默认 10,max 100) */
|
|
20
34
|
count?: number;
|
|
21
35
|
/** diff: 显示暂存区变更(--cached) */
|
package/lib/index.js
CHANGED
|
@@ -611,13 +611,23 @@ function createKitTool(ctx) {
|
|
|
611
611
|
* git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
|
|
612
612
|
*
|
|
613
613
|
* 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
|
|
614
|
-
* - status:JSON {clean, files:[{status,file}], summary}
|
|
615
|
-
* - commit
|
|
616
|
-
* - push
|
|
614
|
+
* - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
|
|
615
|
+
* - commit:默认「只提交指定 paths」;未给 paths ⇒ 整仓 add -A(并在回执里显式告知)
|
|
616
|
+
* - commit:**默认随后 push**(`noPush` 可关);push 结果如实附在回执里
|
|
617
|
+
* - push:成功判据 = **`git push --porcelain` 的 stdout 里读到 ref 行**(绝不采信"stderr 为空")
|
|
617
618
|
* - pull:git fetch + merge --ff-only;up-to-date / [REJECTED] 建议
|
|
618
619
|
* - log:--oneline [-n <count>]
|
|
619
620
|
* - diff:git diff [--cached] [<ref>] [-- <path>]
|
|
620
621
|
* 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
|
|
622
|
+
*
|
|
623
|
+
* 🔴 2026-09-26 修(tiangong-serenity 报障):**push 曾把"超时被杀"报成成功**。
|
|
624
|
+
* 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
|
|
625
|
+
* `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
|
|
626
|
+
* 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
|
|
627
|
+
* 把两者判成同一件事,回显 `Pushed to origin/<branch>` 而远程 ref 从未变。
|
|
628
|
+
* ⇒ 本版三处硬修:① `git()` 交出**结构化结果**(ok/timedOut/status),守卫一律看它,不再看
|
|
629
|
+
* "stderr 是否为空";② push 的成功判据改为**可核验事实**(`--porcelain` 的 ref 行);
|
|
630
|
+
* ③ 超时/未确认一律**不报成功**,并给出 `git ls-remote` 核验指令。
|
|
621
631
|
*/
|
|
622
632
|
const GIT_ACTIONS = [
|
|
623
633
|
"status",
|
|
@@ -627,48 +637,132 @@ const GIT_ACTIONS = [
|
|
|
627
637
|
"pull",
|
|
628
638
|
"diff"
|
|
629
639
|
];
|
|
630
|
-
/**
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
640
|
+
/** 本地动作超时(ms) */
|
|
641
|
+
const DEFAULT_LOCAL_TIMEOUT_MS = 3e4;
|
|
642
|
+
/** 网络动作(fetch/push/pull)超时(ms)——大体量推送可能远超 30s(tiangong 实例:>60s) */
|
|
643
|
+
const DEFAULT_NET_TIMEOUT_MS = 18e4;
|
|
644
|
+
/**
|
|
645
|
+
* 超时可覆盖(`DSH_localTimeout()` / `DSH_netTimeout()`)。
|
|
646
|
+
* 🔵 两个用途:① 慢远端/大体量推送的用户可自行放宽 ② 测试可在**不真等 180s** 的前提下
|
|
647
|
+
* 驱动"超时"这条路径(本仓对它的回归测试就靠这个缝)。
|
|
648
|
+
* ⚠️ **每次调用现读**(不在模块加载时固化)——否则测试改了 env 也影响不到已加载的模块。
|
|
649
|
+
*/
|
|
650
|
+
function timeoutMs(envName, dflt) {
|
|
651
|
+
const v = Number(process.env[envName] ?? "");
|
|
652
|
+
return Number.isFinite(v) && v > 0 ? v : dflt;
|
|
653
|
+
}
|
|
654
|
+
const localTimeout = () => timeoutMs("DSH_GIT_TIMEOUT_MS", DEFAULT_LOCAL_TIMEOUT_MS);
|
|
655
|
+
const netTimeout = () => timeoutMs("DSH_GIT_NET_TIMEOUT_MS", DEFAULT_NET_TIMEOUT_MS);
|
|
656
|
+
/**
|
|
657
|
+
* 执行 git 并交出**结构化结果**。
|
|
658
|
+
* 🔴 超时判据必须用 `code==='ETIMEDOUT' || signal!=null && status==null`——
|
|
659
|
+
* `spawnSync` 超时**不设 `err.killed`**(2026-09-26 实测;旧守卫 `err.killed ? … : err.stderr`
|
|
660
|
+
* 因此是死代码,超时被当成"stderr 为空 = 成功")。
|
|
661
|
+
*/
|
|
662
|
+
function git(root, args, timeoutMs = localTimeout()) {
|
|
634
663
|
try {
|
|
664
|
+
const stdout = execFileSync("git", args, {
|
|
665
|
+
cwd: root,
|
|
666
|
+
encoding: "utf8",
|
|
667
|
+
stdio: [
|
|
668
|
+
"ignore",
|
|
669
|
+
"pipe",
|
|
670
|
+
"pipe"
|
|
671
|
+
],
|
|
672
|
+
maxBuffer: 1048576,
|
|
673
|
+
timeout: timeoutMs
|
|
674
|
+
});
|
|
635
675
|
return {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
"pipe",
|
|
642
|
-
"pipe"
|
|
643
|
-
],
|
|
644
|
-
maxBuffer: 1048576,
|
|
645
|
-
timeout: GIT_TIMEOUT_MS
|
|
646
|
-
}).trimEnd(),
|
|
647
|
-
stderr: ""
|
|
676
|
+
ok: true,
|
|
677
|
+
stdout: String(stdout).trimEnd(),
|
|
678
|
+
stderr: "",
|
|
679
|
+
timedOut: false,
|
|
680
|
+
status: 0
|
|
648
681
|
};
|
|
649
682
|
} catch (err) {
|
|
683
|
+
const timedOut = err?.killed === true || err?.code === "ETIMEDOUT" || err?.signal != null && err?.status == null;
|
|
650
684
|
return {
|
|
651
|
-
|
|
652
|
-
|
|
685
|
+
ok: false,
|
|
686
|
+
stdout: (err?.stdout?.toString() ?? "").trimEnd(),
|
|
687
|
+
stderr: (err?.stderr?.toString() ?? "").trimEnd(),
|
|
688
|
+
timedOut,
|
|
689
|
+
status: typeof err?.status === "number" ? err.status : null
|
|
653
690
|
};
|
|
654
691
|
}
|
|
655
692
|
}
|
|
656
693
|
function getCurrentBranch(root) {
|
|
657
|
-
const
|
|
694
|
+
const r = git(root, [
|
|
658
695
|
"rev-parse",
|
|
659
696
|
"--abbrev-ref",
|
|
660
697
|
"HEAD"
|
|
661
698
|
]);
|
|
662
|
-
if (!stdout) throw new Error("container_git: cannot determine current branch");
|
|
663
|
-
return stdout;
|
|
699
|
+
if (!r.ok || !r.stdout) throw new Error("container_git: cannot determine current branch");
|
|
700
|
+
return r.stdout;
|
|
664
701
|
}
|
|
665
702
|
function hasChanges(root) {
|
|
666
|
-
|
|
667
|
-
|
|
703
|
+
return git(root, [
|
|
704
|
+
"-c",
|
|
705
|
+
"core.quotepath=false",
|
|
706
|
+
"status",
|
|
707
|
+
"--porcelain"
|
|
708
|
+
]).stdout.length > 0;
|
|
668
709
|
}
|
|
669
710
|
function hasRemote(root) {
|
|
670
|
-
|
|
671
|
-
|
|
711
|
+
return git(root, ["remote"]).stdout.length > 0;
|
|
712
|
+
}
|
|
713
|
+
/** 推送成功与否的**唯一**核验指令(给使用者,也写进回执) */
|
|
714
|
+
function verifyHint(branch) {
|
|
715
|
+
return `🔴 核验(唯一判据):git ls-remote origin ${branch},或 git fetch && git rev-parse origin/${branch} —— 与本地 HEAD 比对。**不要采信任何回执。**`;
|
|
716
|
+
}
|
|
717
|
+
const REJECTED_TEXT = (branch) => `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again: container_git push`;
|
|
718
|
+
/**
|
|
719
|
+
* 推送并把结果**如实**翻成文本(`hard` = 作为独立动作时应抛错)。
|
|
720
|
+
* 🔴 成功判据 = `--porcelain` 的 **stdout 里有 ref 行**;退出码 0 但读不到 ref 行 ⇒ **不视为成功**。
|
|
721
|
+
*/
|
|
722
|
+
function pushCore(root) {
|
|
723
|
+
if (!hasRemote(root)) return {
|
|
724
|
+
text: "⚠️ 未配置 remote ⇒ **未推送**(本地提交不受影响)。加 remote: git remote add origin <url>",
|
|
725
|
+
hard: true
|
|
726
|
+
};
|
|
727
|
+
const branch = getCurrentBranch(root);
|
|
728
|
+
git(root, [
|
|
729
|
+
"fetch",
|
|
730
|
+
"origin",
|
|
731
|
+
branch
|
|
732
|
+
], netTimeout());
|
|
733
|
+
const r = git(root, [
|
|
734
|
+
"push",
|
|
735
|
+
"--porcelain",
|
|
736
|
+
"origin",
|
|
737
|
+
branch
|
|
738
|
+
], netTimeout());
|
|
739
|
+
if (r.timedOut) return {
|
|
740
|
+
text: `[TIMEOUT] git push 在 ${netTimeout() / 1e3}s 内未结束 —— 🔴 **推送是否成功【未确认】**(既未成功也未失败)。\n原始输出: ${r.stdout || "(空)"}\n${r.stderr ? `stderr: ${r.stderr}\n` : ""}${verifyHint(branch)}`,
|
|
741
|
+
hard: true
|
|
742
|
+
};
|
|
743
|
+
const refLines = r.stdout.split("\n").filter((l) => /^[ =*+!-]\t/.test(l));
|
|
744
|
+
if (refLines.some((l) => l.startsWith("!"))) return {
|
|
745
|
+
text: REJECTED_TEXT(branch),
|
|
746
|
+
hard: false
|
|
747
|
+
};
|
|
748
|
+
const updated = refLines.filter((l) => /^[ *+-]/.test(l));
|
|
749
|
+
const upToDate = refLines.length > 0 && refLines.every((l) => l.startsWith("="));
|
|
750
|
+
if (updated.length > 0 && !upToDate) return {
|
|
751
|
+
text: `✅ 已推送 origin/${branch}\n${updated.join("\n")}`,
|
|
752
|
+
hard: false
|
|
753
|
+
};
|
|
754
|
+
if (upToDate) return {
|
|
755
|
+
text: `origin/${branch} 已是最新(无可推送的新提交)\n${refLines.join("\n")}`,
|
|
756
|
+
hard: false
|
|
757
|
+
};
|
|
758
|
+
if (!r.ok) return {
|
|
759
|
+
text: `[FAILED] git push 退出码 ${r.status ?? "未知"} —— **未推送**\n${r.stderr || r.stdout || "(无输出)"}\n${verifyHint(branch)}`,
|
|
760
|
+
hard: true
|
|
761
|
+
};
|
|
762
|
+
return {
|
|
763
|
+
text: `[UNVERIFIED] git push 退出码 0,但**未从 --porcelain 读到 ref 行** ⇒ 🔴 **不视为成功**。\n原始输出: ${r.stdout || "(空)"}\n${verifyHint(branch)}`,
|
|
764
|
+
hard: true
|
|
765
|
+
};
|
|
672
766
|
}
|
|
673
767
|
function runGit(root, args) {
|
|
674
768
|
switch (args.action) {
|
|
@@ -679,7 +773,8 @@ function runGit(root, args) {
|
|
|
679
773
|
"status",
|
|
680
774
|
"--porcelain"
|
|
681
775
|
]);
|
|
682
|
-
if (r.
|
|
776
|
+
if (r.timedOut) throw new Error(`status timed out (${localTimeout() / 1e3}s)`);
|
|
777
|
+
if (!r.ok) throw new Error(`status failed: ${r.stderr || r.stdout || "(no output)"}`);
|
|
683
778
|
const files = (r.stdout ? r.stdout.split("\n") : []).map((line) => {
|
|
684
779
|
return {
|
|
685
780
|
status: line.slice(0, 2).trim() || "??",
|
|
@@ -691,6 +786,21 @@ function runGit(root, args) {
|
|
|
691
786
|
files,
|
|
692
787
|
summary: files.length === 0 ? "(clean)" : `${files.length} file(s) with changes`
|
|
693
788
|
};
|
|
789
|
+
if (hasRemote(root)) try {
|
|
790
|
+
const branch = getCurrentBranch(root);
|
|
791
|
+
const rr = git(root, [
|
|
792
|
+
"rev-list",
|
|
793
|
+
"--left-right",
|
|
794
|
+
"--count",
|
|
795
|
+
`origin/${branch}...HEAD`
|
|
796
|
+
]);
|
|
797
|
+
if (rr.ok && /^\d+\s+\d+$/.test(rr.stdout)) {
|
|
798
|
+
const [behind, ahead] = rr.stdout.split(/\s+/).map(Number);
|
|
799
|
+
out.ahead = ahead;
|
|
800
|
+
out.behind = behind;
|
|
801
|
+
if (ahead > 0) out.summary = `${out.summary} | 🔴 本地领先 origin/${branch} **${ahead} 笔未推送**`;
|
|
802
|
+
}
|
|
803
|
+
} catch {}
|
|
694
804
|
const ls = checkLocalstoreGitCompliance(root);
|
|
695
805
|
if (!ls.ok) out.warning = ls.reason;
|
|
696
806
|
return out;
|
|
@@ -699,36 +809,44 @@ function runGit(root, args) {
|
|
|
699
809
|
const ls = checkLocalstoreGitCompliance(root);
|
|
700
810
|
if (!ls.ok) throw new Error(ls.reason);
|
|
701
811
|
if (!args.message || args.message.trim() === "") throw new Error("container_git commit: missing required arg \"message\"");
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
if (
|
|
705
|
-
const
|
|
812
|
+
const paths = (args.paths ?? []).map((p) => p.trim()).filter((p) => p !== "");
|
|
813
|
+
const scoped = paths.length > 0;
|
|
814
|
+
if (!scoped && !hasChanges(root)) return "(nothing to commit — working tree clean)";
|
|
815
|
+
const addR = git(root, scoped ? [
|
|
816
|
+
"add",
|
|
817
|
+
"--",
|
|
818
|
+
...paths
|
|
819
|
+
] : ["add", "-A"]);
|
|
820
|
+
if (addR.timedOut) throw new Error(`container_git commit: git add timed out (${localTimeout() / 1e3}s)`);
|
|
821
|
+
if (!addR.ok) throw new Error(`container_git commit: git add failed\n${addR.stderr || addR.stdout}`);
|
|
822
|
+
const commitR = git(root, scoped ? [
|
|
823
|
+
"commit",
|
|
824
|
+
"-m",
|
|
825
|
+
args.message,
|
|
826
|
+
"--",
|
|
827
|
+
...paths
|
|
828
|
+
] : [
|
|
706
829
|
"commit",
|
|
707
830
|
"-m",
|
|
708
831
|
args.message
|
|
709
832
|
]);
|
|
710
|
-
if (
|
|
711
|
-
if (
|
|
712
|
-
|
|
833
|
+
if (commitR.timedOut) throw new Error(`container_git commit: git commit timed out (${localTimeout() / 1e3}s)`);
|
|
834
|
+
if (`${commitR.stdout}\n${commitR.stderr}`.includes("nothing to commit")) return "(nothing to commit — working tree clean)";
|
|
835
|
+
if (!commitR.ok) throw new Error(`container_git commit: git commit failed\n${commitR.stderr || commitR.stdout}`);
|
|
836
|
+
const lines = [(commitR.stdout || commitR.stderr || "committed").trimEnd()];
|
|
837
|
+
if (!scoped) lines.push("ℹ️ 本次是**整仓**提交(未传 `paths`)—— 包含工作树里**所有**改动,可能含其它轨迹的在途文件。要只提交指定路径 ⇒ 传 `paths`。");
|
|
838
|
+
if (args.noPush) {
|
|
839
|
+
lines.push("⚠️ 按 `noPush` 只提交到**本地** —— 尚未推送到 origin。推送:container_git push");
|
|
840
|
+
return lines.join("\n");
|
|
841
|
+
}
|
|
842
|
+
const p = pushCore(root);
|
|
843
|
+
lines.push(p.hard ? `🔴 **已本地提交,但未推送** ——\n${p.text}` : p.text);
|
|
844
|
+
return lines.join("\n");
|
|
713
845
|
}
|
|
714
846
|
case "push": {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
git(root, [
|
|
719
|
-
"fetch",
|
|
720
|
-
"origin",
|
|
721
|
-
branch
|
|
722
|
-
]);
|
|
723
|
-
} catch {}
|
|
724
|
-
const { stdout, stderr } = git(root, [
|
|
725
|
-
"push",
|
|
726
|
-
"origin",
|
|
727
|
-
branch
|
|
728
|
-
]);
|
|
729
|
-
if (stderr.includes("non-fast-forward") || stderr.includes("rejected") || stderr.includes("[rejected]")) return `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again: container_git push`;
|
|
730
|
-
if (!stderr || stderr.includes("->") || stderr === "") return stdout || `Pushed to origin/${branch}`;
|
|
731
|
-
throw new Error(`container_git push failed:\n${stderr}`);
|
|
847
|
+
const p = pushCore(root);
|
|
848
|
+
if (p.hard) throw new Error(p.text);
|
|
849
|
+
return p.text;
|
|
732
850
|
}
|
|
733
851
|
case "pull": {
|
|
734
852
|
if (!hasRemote(root)) throw new Error("container_git pull: no remote configured. Add one with:\n git remote add origin <url>");
|
|
@@ -737,26 +855,29 @@ function runGit(root, args) {
|
|
|
737
855
|
"fetch",
|
|
738
856
|
"origin",
|
|
739
857
|
branch
|
|
740
|
-
]);
|
|
741
|
-
if (fetchResult.
|
|
858
|
+
], netTimeout());
|
|
859
|
+
if (fetchResult.timedOut) return `[TIMEOUT] git fetch 在 ${netTimeout() / 1e3}s 内未结束 —— **是否已拉到【未确认】**;未做合并。`;
|
|
860
|
+
if (!fetchResult.ok) return `[WARN] fetch failed:\n${fetchResult.stderr || fetchResult.stdout}`;
|
|
742
861
|
const revResult = git(root, [
|
|
743
862
|
"rev-list",
|
|
744
863
|
"--count",
|
|
745
864
|
"HEAD..FETCH_HEAD"
|
|
746
865
|
]);
|
|
747
|
-
if (revResult.
|
|
866
|
+
if (!revResult.ok) return `[WARN] cannot check ahead count:\n${revResult.stderr || revResult.stdout}`;
|
|
748
867
|
if (revResult.stdout === "0" || revResult.stdout === "") return "Already up to date.";
|
|
749
868
|
const mergeResult = git(root, [
|
|
750
869
|
"merge",
|
|
751
870
|
"--ff-only",
|
|
752
871
|
"FETCH_HEAD"
|
|
753
872
|
]);
|
|
754
|
-
if (
|
|
873
|
+
if (mergeResult.timedOut) return `[TIMEOUT] git merge 在 ${localTimeout() / 1e3}s 内未结束 —— **合并是否完成【未确认】**。`;
|
|
874
|
+
if (mergeResult.ok) {
|
|
755
875
|
const msg = mergeResult.stdout || "Pulled successfully.";
|
|
756
876
|
return msg.endsWith("\n") ? msg.trimEnd() : msg;
|
|
757
877
|
}
|
|
758
|
-
|
|
759
|
-
|
|
878
|
+
const mErr = `${mergeResult.stderr}\n${mergeResult.stdout}`;
|
|
879
|
+
if (mErr.includes("non-fast-forward") || mErr.includes("Not possible to fast-forward") || mErr.includes("rejected") || mErr.includes("could not be applied")) return `[REJECTED] Pull from origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local history diverged from remote (non-fast-forward). Suggested actions:\n 1. Inspect differences: container_git log HEAD..origin/${branch}\n 2. Merge manually with bash: git merge origin/${branch}\n 3. Or rebase: git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add <file> && git commit\n 5. Push: container_git push`;
|
|
880
|
+
throw new Error(`container_git pull failed:\n${mergeResult.stderr || mergeResult.stdout}`);
|
|
760
881
|
}
|
|
761
882
|
case "log": {
|
|
762
883
|
const n = Math.min(Math.max(args.count ?? 10, 1), 100);
|
|
@@ -768,7 +889,8 @@ function runGit(root, args) {
|
|
|
768
889
|
"-n",
|
|
769
890
|
String(n)
|
|
770
891
|
]);
|
|
771
|
-
if (r.
|
|
892
|
+
if (r.timedOut) throw new Error(`git log timed out (${localTimeout() / 1e3}s)`);
|
|
893
|
+
if (!r.ok) throw new Error(`git log failed: ${r.stderr || r.stdout || "(no output)"}`);
|
|
772
894
|
if (!r.stdout) return "(no commits)";
|
|
773
895
|
return r.stdout;
|
|
774
896
|
}
|
|
@@ -777,10 +899,11 @@ function runGit(root, args) {
|
|
|
777
899
|
if (args.staged) diffArgs.push("--cached");
|
|
778
900
|
if (args.ref) diffArgs.push(args.ref);
|
|
779
901
|
if (args.path) diffArgs.push("--", args.path);
|
|
780
|
-
const
|
|
781
|
-
if (
|
|
782
|
-
if (!
|
|
783
|
-
return
|
|
902
|
+
const r = git(root, diffArgs);
|
|
903
|
+
if (r.timedOut) throw new Error(`git diff timed out (${localTimeout() / 1e3}s)`);
|
|
904
|
+
if (!r.ok) return `[WARN] git diff failed:\n${r.stderr || r.stdout || "(no output)"}`;
|
|
905
|
+
if (!r.stdout) return "(no diff)";
|
|
906
|
+
return r.stdout;
|
|
784
907
|
}
|
|
785
908
|
default: throw new Error(`Unknown action: ${args.action}`);
|
|
786
909
|
}
|
|
@@ -798,7 +921,7 @@ function renderText$7(value) {
|
|
|
798
921
|
}
|
|
799
922
|
const gitTool = defineTool({
|
|
800
923
|
name: "container_git",
|
|
801
|
-
description: "CCC git operations (cc-git semantics, aligned with osp). status/commit/push/log/pull/diff
|
|
924
|
+
description: "CCC git operations (cc-git semantics, aligned with osp). status/commit/push/log/pull/diff. commit 默认**随后自动 push**(noPush 可关);commit 传 paths ⇒ 只提交这些路径(不传=整仓 add -A,回执会显式告知)。push 成功判据 = --porcelain 的 ref 行(**超时/未确认一律不报成功**,回执给 `git ls-remote` 核验指令);非快进 push/pull 输出 [REJECTED] + 建议(never auto-force)。merge/rebase/conflict resolution go through bash.",
|
|
802
925
|
parameters: {
|
|
803
926
|
action: {
|
|
804
927
|
type: "string",
|
|
@@ -810,6 +933,15 @@ const gitTool = defineTool({
|
|
|
810
933
|
type: "string",
|
|
811
934
|
description: "Commit message (required for commit)"
|
|
812
935
|
},
|
|
936
|
+
paths: {
|
|
937
|
+
type: "array",
|
|
938
|
+
items: { type: "string" },
|
|
939
|
+
description: "commit: 只提交这些路径(如 [\"docs/a.md\",\"src/b.ts\"])。不传 = 整仓 `git add -A`(会吸走其它轨迹的在途文件,回执会显式提示)"
|
|
940
|
+
},
|
|
941
|
+
noPush: {
|
|
942
|
+
type: "boolean",
|
|
943
|
+
description: "commit: 只提交到本地,不随后推送(默认提交后自动推送)"
|
|
944
|
+
},
|
|
813
945
|
count: {
|
|
814
946
|
type: "integer",
|
|
815
947
|
description: "Log count (default 10, max 100; runtime clamp 1-100)"
|
|
@@ -17031,6 +17163,7 @@ function opencodeRouteHeaders() {
|
|
|
17031
17163
|
const OPENCODE_EXTRA_MODELS = { "deepseek-v4.1-flash": {
|
|
17032
17164
|
name: "DeepSeek V4.1 Flash",
|
|
17033
17165
|
api: "openai-completions",
|
|
17166
|
+
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
17034
17167
|
contextWindow: 1e6,
|
|
17035
17168
|
maxTokens: 384e3,
|
|
17036
17169
|
input: ["text"],
|
|
@@ -17074,41 +17207,104 @@ const OPENCODE_GO_NON_COMPLETIONS = /* @__PURE__ */ new Set([
|
|
|
17074
17207
|
"muse-spark-1.2-contributor"
|
|
17075
17208
|
]);
|
|
17076
17209
|
/**
|
|
17077
|
-
*
|
|
17210
|
+
* 把 {@link OPENCODE_EXTRA_MODELS} 落到一个**已有显式 `models` 列表**的路由上(**纯函数**)。
|
|
17211
|
+
*
|
|
17212
|
+
* 两种落法(v1.48.1 起):
|
|
17213
|
+
* · **追加**(模型不在表里)—— 目录里没有它,连条目都得我们造。
|
|
17214
|
+
* · **补规格**(模型已在表里、但缺 `reasoningEfforts`)—— 🔴 **这是"手加过"的形态**:
|
|
17215
|
+
* 用户把模型抄进列表了,可没抄推理档 ⇒ 宿主 `resolveModelReasoning` 走 `base?.reasoning ?? false`
|
|
17216
|
+
* ⇒ **能用但永远不出 thinking**。只补"缺失的模型"是补不到这一形态的。
|
|
17078
17217
|
*
|
|
17079
|
-
*
|
|
17080
|
-
* ① **没有显式 `models`
|
|
17081
|
-
* 一旦我们写出 `models`,**整个目录会被顶掉**(宿主的 `models` 是"整体替换"
|
|
17082
|
-
* ⇒ 宁可不动。
|
|
17218
|
+
* 拒绝条件(每条都对应一个会让配置变坏的真实形态;宁可不动也不硬来):
|
|
17219
|
+
* ① **没有显式 `models` 列表**(含空表)⇒ 该路由此刻服务的是 pi-ai 目录本体;
|
|
17220
|
+
* 一旦我们写出 `models`,**整个目录会被顶掉**(宿主的 `models` 是"整体替换"语义)。
|
|
17083
17221
|
* ② **列表里出现已知异协议模型** ⇒ 设路由级 `api` 会覆盖它们的 `base.api`,把它们配坏。
|
|
17084
17222
|
* ③ **路由已显式设了别的 `api`** ⇒ 同上,且那是用户的显式选择,不覆盖。
|
|
17223
|
+
* ④ **要补规格却判不出协议**(路由没写 `api`,又不追加任何模型)⇒ 无法保证 compat 合法。
|
|
17085
17224
|
*/
|
|
17086
17225
|
function planExtraModels(profile) {
|
|
17087
17226
|
const raw = profile.models;
|
|
17088
17227
|
if (!Array.isArray(raw) || raw.length === 0) return {
|
|
17089
17228
|
addedIds: [],
|
|
17229
|
+
enrichedIds: [],
|
|
17090
17230
|
skip: "catalog-served"
|
|
17091
17231
|
};
|
|
17092
|
-
const
|
|
17093
|
-
for (const entry of raw) if (isPlainObject$1(entry) && typeof entry.id === "string")
|
|
17094
|
-
const
|
|
17095
|
-
if (missing.length === 0) return { addedIds: [] };
|
|
17096
|
-
for (const id of ids) if (OPENCODE_GO_NON_COMPLETIONS.has(id)) return {
|
|
17232
|
+
const byId = /* @__PURE__ */ new Map();
|
|
17233
|
+
for (const entry of raw) if (isPlainObject$1(entry) && typeof entry.id === "string") byId.set(entry.id, entry);
|
|
17234
|
+
for (const id of byId.keys()) if (OPENCODE_GO_NON_COMPLETIONS.has(id)) return {
|
|
17097
17235
|
addedIds: [],
|
|
17236
|
+
enrichedIds: [],
|
|
17098
17237
|
skip: "mixed-protocol"
|
|
17099
17238
|
};
|
|
17100
|
-
const
|
|
17101
|
-
|
|
17239
|
+
const missing = Object.entries(OPENCODE_EXTRA_MODELS).filter(([id]) => !byId.has(id));
|
|
17240
|
+
const existingApi = typeof profile.api === "string" && profile.api !== "" ? profile.api : void 0;
|
|
17241
|
+
let pendingApi;
|
|
17242
|
+
if (missing.length > 0) {
|
|
17243
|
+
const apis = new Set(missing.map(([, model]) => model.api));
|
|
17244
|
+
if (apis.size !== 1) return {
|
|
17245
|
+
addedIds: [],
|
|
17246
|
+
enrichedIds: [],
|
|
17247
|
+
skip: "mixed-protocol"
|
|
17248
|
+
};
|
|
17249
|
+
pendingApi = [...apis][0];
|
|
17250
|
+
}
|
|
17251
|
+
if (existingApi !== void 0 && pendingApi !== void 0 && existingApi !== pendingApi) return {
|
|
17102
17252
|
addedIds: [],
|
|
17103
|
-
|
|
17253
|
+
enrichedIds: [],
|
|
17254
|
+
skip: "api-conflict"
|
|
17104
17255
|
};
|
|
17105
|
-
|
|
17106
|
-
const
|
|
17107
|
-
|
|
17256
|
+
/** 写完之后该路由的**有效协议**(第三方条目也会按它解析) */
|
|
17257
|
+
const effectiveApi = existingApi ?? pendingApi;
|
|
17258
|
+
const toEnrich = [];
|
|
17259
|
+
for (const [id, model] of Object.entries(OPENCODE_EXTRA_MODELS)) {
|
|
17260
|
+
const entry = byId.get(id);
|
|
17261
|
+
if (entry === void 0) continue;
|
|
17262
|
+
if (entry.reasoningEfforts !== void 0) continue;
|
|
17263
|
+
if (effectiveApi === void 0) return {
|
|
17264
|
+
addedIds: [],
|
|
17265
|
+
enrichedIds: [],
|
|
17266
|
+
skip: "api-missing"
|
|
17267
|
+
};
|
|
17268
|
+
if (effectiveApi !== model.api) return {
|
|
17269
|
+
addedIds: [],
|
|
17270
|
+
enrichedIds: [],
|
|
17271
|
+
skip: "api-conflict"
|
|
17272
|
+
};
|
|
17273
|
+
toEnrich.push([
|
|
17274
|
+
id,
|
|
17275
|
+
model,
|
|
17276
|
+
entry
|
|
17277
|
+
]);
|
|
17278
|
+
}
|
|
17279
|
+
if (missing.length === 0 && toEnrich.length === 0) return {
|
|
17108
17280
|
addedIds: [],
|
|
17109
|
-
|
|
17281
|
+
enrichedIds: []
|
|
17110
17282
|
};
|
|
17111
|
-
|
|
17283
|
+
let baseUrl;
|
|
17284
|
+
if (!(typeof profile.baseURL === "string" && profile.baseURL !== "")) {
|
|
17285
|
+
const want = new Set([...missing.map(([, m]) => m), ...toEnrich.map(([, m]) => m)].map((m) => m.baseUrl));
|
|
17286
|
+
if (want.size !== 1) return {
|
|
17287
|
+
addedIds: [],
|
|
17288
|
+
enrichedIds: [],
|
|
17289
|
+
skip: "mixed-protocol"
|
|
17290
|
+
};
|
|
17291
|
+
baseUrl = [...want][0];
|
|
17292
|
+
}
|
|
17293
|
+
const enrichById = new Map(toEnrich.map(([id, model]) => [id, model]));
|
|
17294
|
+
const models = raw.map((entry) => {
|
|
17295
|
+
if (!isPlainObject$1(entry) || typeof entry.id !== "string") return entry;
|
|
17296
|
+
const model = enrichById.get(entry.id);
|
|
17297
|
+
if (model === void 0) return entry;
|
|
17298
|
+
return {
|
|
17299
|
+
...entry,
|
|
17300
|
+
reasoningEfforts: { ...model.reasoningEfforts },
|
|
17301
|
+
compat: {
|
|
17302
|
+
...model.compat,
|
|
17303
|
+
...isPlainObject$1(entry.compat) ? entry.compat : {}
|
|
17304
|
+
}
|
|
17305
|
+
};
|
|
17306
|
+
});
|
|
17307
|
+
for (const [id, model] of missing) models.push({
|
|
17112
17308
|
id,
|
|
17113
17309
|
name: model.name,
|
|
17114
17310
|
contextWindow: model.contextWindow,
|
|
@@ -17116,11 +17312,13 @@ function planExtraModels(profile) {
|
|
|
17116
17312
|
input: [...model.input],
|
|
17117
17313
|
reasoningEfforts: { ...model.reasoningEfforts },
|
|
17118
17314
|
compat: { ...model.compat }
|
|
17119
|
-
})
|
|
17315
|
+
});
|
|
17120
17316
|
return {
|
|
17121
|
-
models
|
|
17122
|
-
...existingApi ===
|
|
17123
|
-
|
|
17317
|
+
models,
|
|
17318
|
+
...pendingApi !== void 0 && existingApi === void 0 ? { api: pendingApi } : {},
|
|
17319
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
17320
|
+
addedIds: missing.map(([id]) => id),
|
|
17321
|
+
enrichedIds: toEnrich.map(([id]) => id)
|
|
17124
17322
|
};
|
|
17125
17323
|
}
|
|
17126
17324
|
function isPlainObject$1(value) {
|
|
@@ -17198,16 +17396,24 @@ function planOpencodeAutoConfig(input) {
|
|
|
17198
17396
|
route,
|
|
17199
17397
|
reason: extra.skip
|
|
17200
17398
|
});
|
|
17201
|
-
else
|
|
17202
|
-
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17399
|
+
else {
|
|
17400
|
+
if (extra.addedIds.length > 0) actions.push({
|
|
17401
|
+
kind: "add-models",
|
|
17402
|
+
route,
|
|
17403
|
+
ids: extra.addedIds,
|
|
17404
|
+
api: extra.api ?? null
|
|
17405
|
+
});
|
|
17406
|
+
if (extra.enrichedIds.length > 0) actions.push({
|
|
17407
|
+
kind: "enrich-models",
|
|
17408
|
+
route,
|
|
17409
|
+
ids: extra.enrichedIds
|
|
17410
|
+
});
|
|
17411
|
+
}
|
|
17207
17412
|
const routePatch = {};
|
|
17208
17413
|
if (keys.length > 0) routePatch.headers = missing;
|
|
17209
17414
|
if (extra.models !== void 0) routePatch.models = extra.models;
|
|
17210
17415
|
if (extra.api !== void 0) routePatch.api = extra.api;
|
|
17416
|
+
if (extra.baseUrl !== void 0) routePatch.baseURL = extra.baseUrl;
|
|
17211
17417
|
if (keys.length === 0 && Object.keys(routePatch).length === 0) {
|
|
17212
17418
|
actions.push({
|
|
17213
17419
|
kind: "skip-route",
|
|
@@ -17249,7 +17455,8 @@ function describeOpencodeAction(action) {
|
|
|
17249
17455
|
case "create-route": return `创建 ${action.route} 路由(检测到 ${OPENCODE_API_KEY_ENV})+ 路由头`;
|
|
17250
17456
|
case "skip-route": return `${action.route} 路由头已齐备(跳过)`;
|
|
17251
17457
|
case "add-models": return `为 ${action.route} 补目录未收录的模型:${action.ids.join(", ")}` + (action.api === null ? "" : `(并设路由 api=${action.api})`);
|
|
17252
|
-
case "
|
|
17458
|
+
case "enrich-models": return `为 ${action.route} 已列出的模型补推理档:${action.ids.join(", ")}(此前「能用但没 thinking」)`;
|
|
17459
|
+
case "skip-models": return `${action.route} 未补模型(${action.reason === "catalog-served" ? "该路由未声明 models 列表,补 models 会顶掉整个目录" : action.reason === "mixed-protocol" ? "该路由含异协议模型,设路由 api 会把它们配坏" : action.reason === "api-missing" ? "该模型已列出但缺推理档,而路由没写 api ⇒ 判不出该用哪套 compat" : "该路由的 api 与本表规格不一致"})`;
|
|
17253
17460
|
case "idle": return `无需动作(${action.reason})`;
|
|
17254
17461
|
}
|
|
17255
17462
|
}
|
|
@@ -94,6 +94,16 @@ export interface OpencodeExtraModel {
|
|
|
94
94
|
readonly name: string;
|
|
95
95
|
/** 该模型说的 wire 协议;见 {@link OPENCODE_GO_NON_COMPLETIONS} 的安全闸 */
|
|
96
96
|
readonly api: string;
|
|
97
|
+
/**
|
|
98
|
+
* 该模型的端点基址 —— 🔴 **目录外模型的必需品**(v1.48.1 补)。
|
|
99
|
+
*
|
|
100
|
+
* 宿主 `resolveRouteModels` 的兜底链是 `request.baseURL ?? base?.baseUrl ?? providerBaseUrl`;
|
|
101
|
+
* 目录里没有它 ⇒ `base` 为 undefined,而 pi-ai 的 `opencodeGoProvider()` **也没有 provider 级
|
|
102
|
+
* baseUrl** ⇒ 三级全空 ⇒ 宿主**严格写入直接把整条补丁拒掉**(v1.48.0 真机实测原文:
|
|
103
|
+
* `model "deepseek-v4.1-flash" needs a baseURL; the installed catalog does not describe this route`)。
|
|
104
|
+
* 故本字段是**唯一可控的那一环**:路由自身没写 `baseURL` 时,必须由我们补上。
|
|
105
|
+
*/
|
|
106
|
+
readonly baseUrl: string;
|
|
97
107
|
readonly contextWindow: number;
|
|
98
108
|
readonly maxTokens: number;
|
|
99
109
|
readonly input: readonly string[];
|
|
@@ -135,10 +145,14 @@ type OpencodeAutoConfigAction = {
|
|
|
135
145
|
route: string;
|
|
136
146
|
ids: readonly string[];
|
|
137
147
|
api: string | null;
|
|
148
|
+
} | {
|
|
149
|
+
kind: 'enrich-models';
|
|
150
|
+
route: string;
|
|
151
|
+
ids: readonly string[];
|
|
138
152
|
} | {
|
|
139
153
|
kind: 'skip-models';
|
|
140
154
|
route: string;
|
|
141
|
-
reason: 'catalog-served' | 'mixed-protocol' | 'api-conflict';
|
|
155
|
+
reason: 'catalog-served' | 'mixed-protocol' | 'api-conflict' | 'api-missing';
|
|
142
156
|
} | {
|
|
143
157
|
kind: 'idle';
|
|
144
158
|
reason: 'namespace-unregistered' | 'no-route';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.48.
|
|
3
|
+
"version": "1.48.2",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|