@lark-apaas/openclaw-scripts-diagnose-cli 0.1.11 → 0.1.12-alpha.0

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.
Files changed (2) hide show
  1. package/dist/index.cjs +2179 -2112
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -52,7 +52,7 @@ node_assert = __toESM(node_assert);
52
52
  * it terse and parseable.
53
53
  */
54
54
  function getVersion() {
55
- return "0.1.11";
55
+ return "0.1.12-alpha.0";
56
56
  }
57
57
  //#endregion
58
58
  //#region src/rule-engine/base.ts
@@ -1546,395 +1546,254 @@ function applyVars(str, entries) {
1546
1546
  return out;
1547
1547
  }
1548
1548
  //#endregion
1549
- //#region src/rules/agents-md-service-commands.ts
1550
- const SERVICE_COMMAND_REPLACEMENTS = [
1551
- ["`sh scripts/start.sh`", "`bash /opt/force/bin/openclaw_scripts/start.sh`"],
1552
- ["`sh scripts/restart.sh`", "`bash /opt/force/bin/openclaw_scripts/restart.sh`"],
1553
- ["`sh scripts/stop.sh`", "`bash /opt/force/bin/openclaw_scripts/stop.sh`"]
1554
- ];
1555
- let AgentsMdServiceCommandsRule = class AgentsMdServiceCommandsRule extends DiagnoseRule {
1549
+ //#region src/rules/gateway.ts
1550
+ const DEFAULT_PORT = 18789;
1551
+ const DEFAULT_MODE = "local";
1552
+ const DEFAULT_BIND = "loopback";
1553
+ const DEFAULT_AUTH_MODE = "token";
1554
+ const DEFAULT_AUTH_TOKEN = {
1555
+ source: "file",
1556
+ provider: "miaoda-secret-provider",
1557
+ id: "/gateway_auth_token"
1558
+ };
1559
+ /** Required entries in gateway.trustedProxies. Repair appends any missing
1560
+ * entries while preserving caller-added extras (no overwrite). */
1561
+ const DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
1562
+ let GatewayRule = class GatewayRule extends DiagnoseRule {
1556
1563
  validate(ctx) {
1557
- const agentsMdPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
1558
- return hasOldServiceCommands(node_fs.default.readFileSync(filePath, "utf-8"));
1559
- });
1560
- if (!agentsMdPath) return { pass: true };
1561
- return {
1564
+ const gateway = ctx.config.gateway;
1565
+ if (!gateway || typeof gateway !== "object") return {
1562
1566
  pass: false,
1563
- message: `${agentsMdPath} 中存在旧版服务命令,需要改为 /opt/force/bin/openclaw_scripts 路径`
1567
+ message: "gateway not found"
1568
+ };
1569
+ const gw = gateway;
1570
+ if (gw.port !== DEFAULT_PORT) return {
1571
+ pass: false,
1572
+ message: "gateway.port mismatch: got " + gw.port + ", expected 18789"
1573
+ };
1574
+ if (gw.mode !== DEFAULT_MODE) return {
1575
+ pass: false,
1576
+ message: "gateway.mode mismatch: got " + gw.mode + ", expected local"
1577
+ };
1578
+ if (gw.bind !== DEFAULT_BIND) return {
1579
+ pass: false,
1580
+ message: "gateway.bind mismatch: got " + gw.bind + ", expected loopback"
1581
+ };
1582
+ const auth = gw.auth;
1583
+ if (!auth || typeof auth !== "object") return {
1584
+ pass: false,
1585
+ message: "gateway.auth not found"
1586
+ };
1587
+ const authObj = auth;
1588
+ if (authObj.mode !== DEFAULT_AUTH_MODE) return {
1589
+ pass: false,
1590
+ message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected token"
1591
+ };
1592
+ const token = authObj.token;
1593
+ if (typeof token === "string") {
1594
+ if (token !== ctx.vars.gatewayToken) return {
1595
+ pass: false,
1596
+ message: "gateway.auth.token string value mismatch"
1597
+ };
1598
+ } else if (typeof token === "object" && token !== null && !Array.isArray(token)) {
1599
+ if (!matchMap(token, DEFAULT_AUTH_TOKEN)) return {
1600
+ pass: false,
1601
+ message: "gateway.auth.token object mismatch: got " + JSON.stringify(token)
1602
+ };
1603
+ } else return {
1604
+ pass: false,
1605
+ message: "gateway.auth.token has unexpected type"
1606
+ };
1607
+ const controlUi = gw.controlUi;
1608
+ if (!controlUi || typeof controlUi !== "object") return {
1609
+ pass: false,
1610
+ message: "gateway.controlUi not found"
1611
+ };
1612
+ if (controlUi.dangerouslyDisableDeviceAuth !== true) return {
1613
+ pass: false,
1614
+ message: "gateway.controlUi.dangerouslyDisableDeviceAuth must be true, got " + controlUi.dangerouslyDisableDeviceAuth
1615
+ };
1616
+ const proxies = gw.trustedProxies;
1617
+ if (!Array.isArray(proxies)) return {
1618
+ pass: false,
1619
+ message: "gateway.trustedProxies missing or not an array"
1620
+ };
1621
+ const missing = DEFAULT_TRUSTED_PROXIES.filter((p) => !proxies.includes(p));
1622
+ if (missing.length > 0) return {
1623
+ pass: false,
1624
+ message: "gateway.trustedProxies missing: " + JSON.stringify(missing)
1564
1625
  };
1626
+ return { pass: true };
1565
1627
  }
1566
1628
  repair(ctx) {
1567
- for (const agentsMdPath of collectExistingAgentsMdPaths(ctx)) {
1568
- const content = node_fs.default.readFileSync(agentsMdPath, "utf-8");
1569
- const next = replaceOldServiceCommands(content);
1570
- if (next !== content) node_fs.default.writeFileSync(agentsMdPath, next, "utf-8");
1629
+ setNestedValue(ctx.config, ["gateway", "port"], DEFAULT_PORT);
1630
+ setNestedValue(ctx.config, ["gateway", "mode"], DEFAULT_MODE);
1631
+ setNestedValue(ctx.config, ["gateway", "bind"], DEFAULT_BIND);
1632
+ setNestedValue(ctx.config, [
1633
+ "gateway",
1634
+ "auth",
1635
+ "mode"
1636
+ ], DEFAULT_AUTH_MODE);
1637
+ setNestedValue(ctx.config, [
1638
+ "gateway",
1639
+ "auth",
1640
+ "token"
1641
+ ], DEFAULT_AUTH_TOKEN);
1642
+ setNestedValue(ctx.config, [
1643
+ "gateway",
1644
+ "controlUi",
1645
+ "dangerouslyDisableDeviceAuth"
1646
+ ], true);
1647
+ const gw = ctx.config.gateway ?? {};
1648
+ const current = Array.isArray(gw.trustedProxies) ? gw.trustedProxies.slice() : [];
1649
+ const seen = new Set(current.map((v) => String(v)));
1650
+ for (const p of DEFAULT_TRUSTED_PROXIES) if (!seen.has(p)) {
1651
+ current.push(p);
1652
+ seen.add(p);
1571
1653
  }
1654
+ setNestedValue(ctx.config, ["gateway", "trustedProxies"], current);
1572
1655
  }
1573
1656
  };
1574
- AgentsMdServiceCommandsRule = __decorate([Rule({
1575
- key: "agents_md_service_commands",
1576
- description: "检测各智能体 AGENTS.md 中旧版 OpenClaw 服务命令,并替换为 /opt/force/bin/openclaw_scripts 路径",
1657
+ GatewayRule = __decorate([Rule({
1658
+ key: "gateway",
1659
+ description: "验证 gateway 必填字段(port、mode、bind、auth、trustedProxies)是否存在且有效;修复缺失或非法值",
1577
1660
  dependsOn: ["config_syntax_check"],
1578
1661
  repairMode: "standard",
1579
- level: "silent"
1580
- })], AgentsMdServiceCommandsRule);
1581
- function hasOldServiceCommands(content) {
1582
- return SERVICE_COMMAND_REPLACEMENTS.some(([oldCmd]) => content.includes(oldCmd));
1583
- }
1584
- function replaceOldServiceCommands(content) {
1585
- let next = content;
1586
- for (const [oldCmd, newCmd] of SERVICE_COMMAND_REPLACEMENTS) next = next.split(oldCmd).join(newCmd);
1587
- return next;
1588
- }
1662
+ level: "critical",
1663
+ usesVars: ["gatewayToken"]
1664
+ })], GatewayRule);
1589
1665
  //#endregion
1590
- //#region src/rules/agents-md-resource-constrained-tools.ts
1591
- const RESOURCE_CONSTRAINED_TOOLS_TAG = "resource_constrained_tools";
1592
- const RESOURCE_CONSTRAINED_TOOLS_BLOCK = [
1593
- "<resource_constrained_tools>",
1594
- "### Browser tab 管理",
1595
- "Chrome tab 是沙箱里的**头号资源杀手**:在线上 OpenClaw 集群的 Mem Top10 里占 42%、CPU Top10 里占 70%。下面规则**全部**指 `browser` 工具自身的 action(不是别的 MCP),按\"调用前 / 调用后 / 自查 / 兜底\"四阶段顺序遵守,不要跳。",
1596
- "#### 调用 `browser action: \"open\"` 之前——必答两问",
1597
- "1. 我上一次 `open` 的 tab 是否还活着?活着 → 用 `action: \"navigate\"` + 它的 `targetId` 换 URL,**不要新开**。",
1598
- "2. 完成这次访问后,我下一步能不能立刻 `close`?不能 → 拆小任务,先不开。",
1599
- "#### 调用 `browser action: \"open\"` 之后——下一个 `browser` 工具调用必须是以下之一",
1600
- "- `action: \"navigate\"`(带 open 返回的 `targetId`):复用此 tab 换 URL。",
1601
- "- `action: \"close\"`(带 open 返回的 `targetId`):关闭此 tab,任务结束。",
1602
- "- `action: \"snapshot\"` / `\"click\"` / `\"type\"` 等:仅当**当前任务仍在这个 tab 上推进**时可用;任务做完仍必须显式 `close` 并传 `targetId`。",
1603
- "**严禁**下面这种序列(线上实测到的 chrome 内存 Top1 故障模式):",
1604
- " BAD:",
1605
- " browser(open, url=A) → snapshot",
1606
- " → browser(open, url=B) → snapshot",
1607
- " → browser(open, url=C) → snapshot",
1608
- " → …(任务结束未关)",
1609
- " # 每次 open 都新建一个 tab;snapshot 不会关;chrome 堆栈式泄漏。",
1610
- "应该写成:",
1611
- " GOOD:",
1612
- " browser(open, url=A) → snapshot",
1613
- " → browser(navigate, targetId=t1, url=B) → snapshot",
1614
- " → … → browser(close, targetId=t1)",
1615
- " # 始终复用同一个 tab;任务结束显式 close。",
1616
- "#### 异常时止损",
1617
- "发现 Chrome 卡顿、心跳延迟、沙箱负载升高:先 `close` 所有非活跃 tab;仍不行就让 shell 杀掉 chrome 进程(`pkill -INT chromium-browser` 或等价命令)让它按需重启,**不要**继续开新 tab。",
1618
- "### FFmpeg / 媒体编码使用限制",
1619
- "ffmpeg 默认吃满所有 CPU 核心,沙箱只有 1–2 vCPU,一次无约束转码就能拖垮心跳和 Browser。下面的规则**只针对 shell 直接调用的 `ffmpeg` / `ffprobe`**:",
1620
- "- **优先用 skill:** `skills/video-frames` 等媒体 skill 是已审查过的窄用例,跟着它们的写法走比自己手撸 ffmpeg 安全;只在 skill 不覆盖时才直接 shell `ffmpeg`。注意 `video-frames` 抽中后段帧用 `--time`(demuxer seek,几乎零 CPU)而非 `--index N`(会从头解码 N 帧)。",
1621
- "- **先 probe 再 encode:** 转码前先 `ffprobe -v error -threads 1 -show_streams -show_format -of json <input>` 拿到时长、码率、编码;能用 `-c copy` 流拷贝就绝不重编码;时长超 1200s 先 `-t` 截断或拒绝任务。",
1622
- "- **强制限线程:** 必须显式带 `-threads 1`(最多 `-threads 2`),滤镜链追加 `-filter_threads 1 -filter_complex_threads 1`。",
1623
- "- **软件编码必须 ultrafast:** x264/x265 统一 `-preset ultrafast`,禁止 `medium` 及以上预设;质量不够时降分辨率(`-vf scale=-2:720`)或降帧率(`-r 24`),不要靠提高 preset。",
1624
- "- **禁止并发 ffmpeg:** 同一沙箱内同时只允许一个 ffmpeg 进程,开下一个前用 `pgrep -x ffmpeg` 确认无残留;禁止 `xargs -P`、禁止循环里并行批处理。",
1625
- "- **必须前台 + 超时兜底:** 用 `timeout 45 ffmpeg -nostdin -hide_banner -loglevel error ...` 包一层(与上游 `MEDIA_FFMPEG_TIMEOUT_MS` 对齐,最多放宽到 `timeout 90`),ffprobe 用 `timeout 10`;禁止 `&` 后台、禁止 `nohup`,跑飞时必须能 Ctrl-C 打断。",
1626
- "- **中间产物即用即删:** 临时切片、调色板、`-pass 1` log 等,任务结束或失败时立即 `rm -f`,不要把 `/tmp/*.mp4`、`ffmpeg2pass-*.log` 留到下一个任务。",
1627
- "- **异常时硬停:** 沙箱变慢、心跳丢失、或 `frame=` 长时间不前进,立即 `pkill -INT ffmpeg`(不行就 `-KILL`);**不要**重试相同命令,先降分辨率/preset 或改用流拷贝。",
1628
- "</resource_constrained_tools>"
1629
- ].join("\n");
1630
- let AgentsMdResourceConstrainedToolsRule = class AgentsMdResourceConstrainedToolsRule extends DiagnoseRule {
1666
+ //#region src/rules/allowed-origins.ts
1667
+ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
1631
1668
  validate(ctx) {
1632
- const stalePath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
1633
- return !hasCurrentResourceConstrainedToolsBlock(node_fs.default.readFileSync(filePath, "utf-8"));
1634
- });
1635
- if (!stalePath) return { pass: true };
1669
+ const expected = getExpectedOrigins(ctx.vars);
1670
+ if (expected.length === 0) return { pass: true };
1671
+ const current = getCurrentOrigins(ctx.config);
1672
+ if (hasWildcard(current)) return { pass: true };
1673
+ const missing = findMissing(current, expected);
1674
+ if (missing.length === 0) return { pass: true };
1636
1675
  return {
1637
1676
  pass: false,
1638
- message: `${stalePath} 中缺少或存在旧版 resource_constrained_tools,需要更新`
1677
+ message: "allowedOrigins missing: " + JSON.stringify(missing)
1639
1678
  };
1640
1679
  }
1641
1680
  repair(ctx) {
1642
- for (const filePath of collectExistingAgentsMdPaths(ctx)) {
1643
- const content = node_fs.default.readFileSync(filePath, "utf-8");
1644
- const next = upsertResourceConstrainedToolsBlock(content);
1645
- if (next !== content) node_fs.default.writeFileSync(filePath, next, "utf-8");
1681
+ const expected = getExpectedOrigins(ctx.vars);
1682
+ const current = getCurrentOrigins(ctx.config);
1683
+ if (hasWildcard(current)) return;
1684
+ const missing = findMissing(current, expected);
1685
+ if (missing.length > 0) {
1686
+ const seen = /* @__PURE__ */ new Set();
1687
+ const merged = [];
1688
+ for (const o of current) if (!seen.has(o)) {
1689
+ merged.push(o);
1690
+ seen.add(o);
1691
+ }
1692
+ for (const o of missing) if (!seen.has(o)) {
1693
+ merged.push(o);
1694
+ seen.add(o);
1695
+ }
1696
+ setNestedValue(ctx.config, [
1697
+ "gateway",
1698
+ "controlUi",
1699
+ "allowedOrigins"
1700
+ ], merged);
1646
1701
  }
1647
1702
  }
1648
1703
  };
1649
- AgentsMdResourceConstrainedToolsRule = __decorate([Rule({
1650
- key: "agents_md_resource_constrained_tools",
1651
- description: "检测各智能体 AGENTS.md resource_constrained_tools 规则块,缺失时追加,存在时替换为当前内容",
1704
+ AllowedOriginsRule = __decorate([Rule({
1705
+ key: "allowed_origins",
1706
+ description: "确保所有 expectedOrigins 条目都存在于 gateway.auth.allowedOrigins 中;自动追加缺失的条目",
1652
1707
  dependsOn: ["config_syntax_check"],
1653
1708
  repairMode: "standard",
1654
- level: "silent"
1655
- })], AgentsMdResourceConstrainedToolsRule);
1656
- function hasCurrentResourceConstrainedToolsBlock(content) {
1657
- return normalizeForTemplateMatch(content).includes(normalizeForTemplateMatch(RESOURCE_CONSTRAINED_TOOLS_BLOCK));
1709
+ level: "critical",
1710
+ usesVars: ["expectedOrigins"]
1711
+ })], AllowedOriginsRule);
1712
+ function getExpectedOrigins(vars) {
1713
+ return Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
1658
1714
  }
1659
- function normalizeForTemplateMatch(content) {
1660
- return content.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).join("\n");
1715
+ function getCurrentOrigins(config) {
1716
+ const controlUi = getNestedMap(config, "gateway", "controlUi");
1717
+ if (!controlUi) return [];
1718
+ const raw = controlUi.allowedOrigins;
1719
+ if (!Array.isArray(raw)) return [];
1720
+ return raw.filter((o) => typeof o === "string");
1661
1721
  }
1662
- function upsertResourceConstrainedToolsBlock(content) {
1663
- const tagRe = new RegExp(`<${RESOURCE_CONSTRAINED_TOOLS_TAG}>[\\s\\S]*?</${RESOURCE_CONSTRAINED_TOOLS_TAG}>`);
1664
- if (tagRe.test(content)) return content.replace(tagRe, RESOURCE_CONSTRAINED_TOOLS_BLOCK);
1665
- return `${content}${content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n"}${RESOURCE_CONSTRAINED_TOOLS_BLOCK}\n`;
1722
+ function findMissing(current, expected) {
1723
+ const set = new Set(current);
1724
+ return expected.filter((o) => !set.has(o));
1725
+ }
1726
+ /** Exact "*" entry means allow-all; pattern globs like "https://*.example.com" don't count. */
1727
+ function hasWildcard(origins) {
1728
+ return origins.includes("*");
1666
1729
  }
1667
1730
  //#endregion
1668
- //#region src/rules/model-provider.ts
1669
- const DEFAULT_API_KEY = {
1670
- source: "file",
1671
- provider: "miaoda-provider",
1672
- id: "value"
1673
- };
1674
- const DEFAULT_X_API_KEY_HEADER = {
1675
- source: "file",
1676
- provider: "miaoda-secret-provider",
1677
- id: "/models_providers_miaoda_headers_x_api_key"
1678
- };
1679
- const DEFAULT_API = "openai-completions";
1680
- function getExpected(vars) {
1681
- return {
1682
- baseUrl: vars.baseURL + "/api/v1/sgw/model/proxy",
1683
- apiKey: DEFAULT_API_KEY,
1684
- api: DEFAULT_API,
1685
- headers: { "x-api-key": DEFAULT_X_API_KEY_HEADER }
1686
- };
1687
- }
1688
- let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
1689
- validate(ctx) {
1690
- const provider = getNestedMap(ctx.config, "models", "providers", "miaoda");
1691
- if (!provider) return {
1692
- pass: false,
1693
- message: "models.providers.miaoda not found"
1694
- };
1695
- const expected = getExpected(ctx.vars);
1696
- if (provider.baseUrl !== expected.baseUrl) return {
1697
- pass: false,
1698
- message: "baseUrl mismatch: got " + provider.baseUrl + ", expected " + expected.baseUrl
1699
- };
1700
- const expectedApiKey = expected.apiKey && typeof expected.apiKey === "object" ? expected.apiKey : null;
1701
- if (expectedApiKey) if (typeof provider.apiKey === "object" && provider.apiKey !== null && !Array.isArray(provider.apiKey)) {
1702
- if (!matchMap(provider.apiKey, expectedApiKey)) return {
1703
- pass: false,
1704
- message: "apiKey object mismatch: got " + JSON.stringify(provider.apiKey)
1705
- };
1706
- } else if (typeof provider.apiKey === "string") {
1707
- if (!isValidJWT(provider.apiKey)) return {
1708
- pass: false,
1709
- message: "apiKey is a string but not a valid/unexpired JWT"
1710
- };
1711
- } else return {
1712
- pass: false,
1713
- message: "apiKey has unexpected type"
1714
- };
1715
- if (expected.api !== void 0) {
1716
- if (provider.api !== expected.api) return {
1717
- pass: false,
1718
- message: "api mismatch: got " + provider.api + ", expected " + expected.api
1719
- };
1720
- }
1721
- const expectedHeaders = getNestedMap(expected, "headers");
1722
- const expectedXApiKey = expectedHeaders ? expectedHeaders["x-api-key"] : void 0;
1723
- if (expectedXApiKey && typeof expectedXApiKey === "object") {
1724
- const xKey = (typeof provider.headers === "object" && provider.headers || {})["x-api-key"];
1725
- if (typeof xKey === "object" && xKey !== null && !Array.isArray(xKey)) {
1726
- if (!matchMap(xKey, expectedXApiKey)) return {
1727
- pass: false,
1728
- message: "headers.x-api-key object mismatch: got " + JSON.stringify(xKey)
1729
- };
1730
- } else if (typeof xKey === "string") {
1731
- if (xKey !== ctx.vars.innerAPIKey) return {
1732
- pass: false,
1733
- message: "headers.x-api-key string value mismatch"
1734
- };
1735
- } else return {
1736
- pass: false,
1737
- message: "headers.x-api-key has unexpected type"
1738
- };
1739
- }
1740
- return { pass: true };
1741
- }
1742
- repair(ctx) {
1743
- const expected = getExpected(ctx.vars);
1744
- setNestedValue(ctx.config, [
1745
- "models",
1746
- "providers",
1747
- "miaoda",
1748
- "baseUrl"
1749
- ], expected.baseUrl);
1750
- if (expected.apiKey !== void 0) setNestedValue(ctx.config, [
1751
- "models",
1752
- "providers",
1753
- "miaoda",
1754
- "apiKey"
1755
- ], expected.apiKey);
1756
- if (expected.api !== void 0) setNestedValue(ctx.config, [
1757
- "models",
1758
- "providers",
1759
- "miaoda",
1760
- "api"
1761
- ], expected.api);
1762
- if (expected.headers !== void 0) setNestedValue(ctx.config, [
1763
- "models",
1764
- "providers",
1765
- "miaoda",
1766
- "headers"
1767
- ], expected.headers);
1731
+ //#region src/rules/session-persistence.ts
1732
+ const DEFAULT_SESSION = {
1733
+ resetByChannel: { feishu: {
1734
+ mode: "idle",
1735
+ idleMinutes: 10080
1736
+ } },
1737
+ maintenance: {
1738
+ mode: "enforce",
1739
+ resetArchiveRetention: "7d",
1740
+ maxDiskBytes: "5GB"
1768
1741
  }
1769
1742
  };
1770
- ModelProviderRule = __decorate([Rule({
1771
- key: "model_provider",
1772
- description: "检查妙搭 model provider 配置项(baseUrl、apiKey、必要 headers)是否存在且格式正确;非妙搭沙箱时跳过",
1773
- dependsOn: ["config_syntax_check"],
1774
- repairMode: "standard",
1775
- level: "critical",
1776
- usesVars: ["innerAPIKey", "baseURL"],
1777
- skipWhen: ({ hasMiaoda }) => !hasMiaoda
1778
- })], ModelProviderRule);
1779
- //#endregion
1780
- //#region src/rules/secret-provider.ts
1781
- var _SecretProviderRule;
1782
- let SecretProviderRule = class SecretProviderRule extends DiagnoseRule {
1783
- static {
1784
- _SecretProviderRule = this;
1785
- }
1786
- static DEFAULT_MIAODA_PROVIDER = {
1787
- source: "file",
1788
- path: "/home/gem/workspace/.force/openclaw/miaoda-provider-key",
1789
- mode: "singleValue"
1790
- };
1791
- static DEFAULT_MIAODA_SECRET_PROVIDER = {
1792
- source: "file",
1793
- path: "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json",
1794
- mode: "json"
1795
- };
1743
+ let SessionPersistenceRule = class SessionPersistenceRule extends DiagnoseRule {
1796
1744
  validate(ctx) {
1797
- const providers = getNestedMap(ctx.config, "secrets", "providers");
1798
- if (!providers) return {
1745
+ const session = getNestedMap(ctx.config, "session");
1746
+ const issues = [];
1747
+ if (!getNestedMap(session, "resetByChannel")) issues.push("session.resetByChannel missing");
1748
+ if (!getNestedMap(session, "maintenance")) issues.push("session.maintenance missing");
1749
+ if (issues.length === 0) return { pass: true };
1750
+ return {
1799
1751
  pass: false,
1800
- message: "secrets.providers not found"
1752
+ message: issues.join("; ")
1801
1753
  };
1802
- const { mp: expectedMP, msp: expectedMSP } = this.getExpected();
1803
- if (ctx.providerDeps.usesMiaodaProvider) {
1804
- const mp = providers["miaoda-provider"];
1805
- if (!mp || typeof mp !== "object" || !matchMap(mp, expectedMP)) return {
1806
- pass: false,
1807
- message: "secrets.providers.miaoda-provider mismatch: got " + JSON.stringify(mp)
1808
- };
1809
- }
1810
- if (ctx.providerDeps.usesMiaodaSecretProvider) {
1811
- const msp = providers["miaoda-secret-provider"];
1812
- if (!msp || typeof msp !== "object" || !matchMap(msp, expectedMSP)) return {
1813
- pass: false,
1814
- message: "secrets.providers.miaoda-secret-provider mismatch: got " + JSON.stringify(msp)
1815
- };
1816
- }
1817
- return { pass: true };
1818
1754
  }
1819
1755
  repair(ctx) {
1820
- const { mp: expectedMP, msp: expectedMSP } = this.getExpected();
1821
- if (ctx.providerDeps.usesMiaodaProvider) setNestedValue(ctx.config, [
1822
- "secrets",
1823
- "providers",
1824
- "miaoda-provider"
1825
- ], expectedMP);
1826
- if (ctx.providerDeps.usesMiaodaSecretProvider) setNestedValue(ctx.config, [
1827
- "secrets",
1828
- "providers",
1829
- "miaoda-secret-provider"
1830
- ], expectedMSP);
1831
- }
1832
- getExpected() {
1833
- return {
1834
- mp: _SecretProviderRule.DEFAULT_MIAODA_PROVIDER,
1835
- msp: _SecretProviderRule.DEFAULT_MIAODA_SECRET_PROVIDER
1836
- };
1756
+ const session = getNestedMap(ctx.config, "session") ?? {};
1757
+ if (!getNestedMap(ctx.config, "session")) ctx.config.session = session;
1758
+ const needsResetByChannel = !getNestedMap(session, "resetByChannel");
1759
+ const needsMaintenance = !getNestedMap(session, "maintenance");
1760
+ if (!needsResetByChannel && !needsMaintenance) return;
1761
+ if (needsResetByChannel) session.resetByChannel = DEFAULT_SESSION.resetByChannel;
1762
+ if (needsMaintenance) session.maintenance = DEFAULT_SESSION.maintenance;
1837
1763
  }
1838
1764
  };
1839
- SecretProviderRule = _SecretProviderRule = __decorate([Rule({
1840
- key: "secret_provider",
1841
- description: "验证 miaoda-provider / miaoda-secret-provider 配置块是否存在且完整;未检测到妙搭 provider 时跳过",
1765
+ SessionPersistenceRule = __decorate([Rule({
1766
+ key: "session_persistence",
1767
+ description: "补齐 session.resetByChannel session.maintenance 持久化配置",
1842
1768
  dependsOn: ["config_syntax_check"],
1843
1769
  repairMode: "standard",
1844
- level: "critical",
1845
- skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider && !deps.usesMiaodaSecretProvider
1846
- })], SecretProviderRule);
1770
+ level: "silent"
1771
+ })], SessionPersistenceRule);
1847
1772
  //#endregion
1848
- //#region src/rules/feishu-channel.ts
1773
+ //#region src/rules/feishu-default-account.ts
1849
1774
  /**
1850
- * Owns `channels.feishu.enabled` + single-agent top-level appId/appSecret.
1851
- * Multi-agent shape (`accounts` present) belongs to `feishu_default_account`.
1775
+ * Owns the multi-agent feishu-channel migration: turns legacy v1/v2
1776
+ * (top-level appId + defaultAccount/default) into v3 (`bot-<appId>` account),
1777
+ * preserving the user's top-level `appId` / `appSecret` verbatim. Once on
1778
+ * v3 this rule does not touch account values — the user owns them.
1779
+ * Single-agent configs (no `accounts`) are out of scope — handled by
1780
+ * `feishu_channel`.
1852
1781
  */
1853
- let FeishuChannelRule = class FeishuChannelRule extends DiagnoseRule {
1782
+ /** Top-level `channels.feishu.*` account-policy fields migrated into the main bot. */
1783
+ const TOP_FIELDS_TO_MIGRATE = [
1784
+ "dmPolicy",
1785
+ "allowFrom",
1786
+ "groupPolicy",
1787
+ "groupAllowFrom"
1788
+ ];
1789
+ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRule {
1854
1790
  validate(ctx) {
1855
1791
  const feishu = getNestedMap(ctx.config, "channels", "feishu");
1856
- if (!feishu) return {
1857
- pass: false,
1858
- message: "channels.feishu not found"
1859
- };
1860
- if (feishu.enabled !== true) return {
1861
- pass: false,
1862
- message: "channels.feishu.enabled mismatch: got " + feishu.enabled + ", expected true"
1863
- };
1864
- if (asRecord(feishu.accounts)) return { pass: true };
1865
- if (feishu.appId !== ctx.vars.feishuAppID) return {
1866
- pass: false,
1867
- message: `channels.feishu.appId mismatch: got ${feishu.appId}, expected ${ctx.vars.feishuAppID}`
1868
- };
1869
- const secret = feishu.appSecret;
1870
- if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
1871
- if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) return {
1872
- pass: false,
1873
- message: `channels.feishu.appSecret object mismatch: got ${JSON.stringify(secret)}`
1874
- };
1875
- } else if (typeof secret === "string") {
1876
- if (secret !== ctx.vars.feishuAppSecret) return {
1877
- pass: false,
1878
- message: "channels.feishu.appSecret string value mismatch"
1879
- };
1880
- } else return {
1792
+ if (!feishu) return { pass: true };
1793
+ if (!asRecord(feishu.accounts)) return { pass: true };
1794
+ if (hasLegacyTopLevel(feishu)) return {
1881
1795
  pass: false,
1882
- message: "channels.feishu.appSecret has unexpected type"
1883
- };
1884
- return { pass: true };
1885
- }
1886
- repair(ctx) {
1887
- setNestedValue(ctx.config, [
1888
- "channels",
1889
- "feishu",
1890
- "enabled"
1891
- ], true);
1892
- if (asRecord(getNestedMap(ctx.config, "channels", "feishu").accounts)) return;
1893
- setNestedValue(ctx.config, [
1894
- "channels",
1895
- "feishu",
1896
- "appId"
1897
- ], ctx.vars.feishuAppID);
1898
- setNestedValue(ctx.config, [
1899
- "channels",
1900
- "feishu",
1901
- "appSecret"
1902
- ], DEFAULT_FEISHU_APP_SECRET);
1903
- }
1904
- };
1905
- FeishuChannelRule = __decorate([Rule({
1906
- key: "feishu_channel",
1907
- description: "确保 channels.feishu 已启用,且包含单 agent 所需的 appId 和 appSecret 字段",
1908
- dependsOn: ["config_syntax_check", "feishu_default_account"],
1909
- repairMode: "standard",
1910
- level: "critical",
1911
- usesVars: ["feishuAppID", "feishuAppSecret"]
1912
- })], FeishuChannelRule);
1913
- //#endregion
1914
- //#region src/rules/feishu-default-account.ts
1915
- /**
1916
- * Owns the multi-agent feishu-channel migration: turns legacy v1/v2
1917
- * (top-level appId + defaultAccount/default) into v3 (`bot-<appId>` account),
1918
- * preserving the user's top-level `appId` / `appSecret` verbatim. Once on
1919
- * v3 this rule does not touch account values — the user owns them.
1920
- * Single-agent configs (no `accounts`) are out of scope — handled by
1921
- * `feishu_channel`.
1922
- */
1923
- /** Top-level `channels.feishu.*` account-policy fields migrated into the main bot. */
1924
- const TOP_FIELDS_TO_MIGRATE = [
1925
- "dmPolicy",
1926
- "allowFrom",
1927
- "groupPolicy",
1928
- "groupAllowFrom"
1929
- ];
1930
- let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRule {
1931
- validate(ctx) {
1932
- const feishu = getNestedMap(ctx.config, "channels", "feishu");
1933
- if (!feishu) return { pass: true };
1934
- if (!asRecord(feishu.accounts)) return { pass: true };
1935
- if (hasLegacyTopLevel(feishu)) return {
1936
- pass: false,
1937
- message: "channels.feishu has legacy shape; needs migration to accounts.bot-<appId>"
1796
+ message: "channels.feishu has legacy shape; needs migration to accounts.bot-<appId>"
1938
1797
  };
1939
1798
  return { pass: true };
1940
1799
  }
@@ -2042,6 +1901,72 @@ function hasLegacyTopLevel(feishu) {
2042
1901
  return feishu.appSecret !== void 0;
2043
1902
  }
2044
1903
  //#endregion
1904
+ //#region src/rules/feishu-channel.ts
1905
+ /**
1906
+ * Owns `channels.feishu.enabled` + single-agent top-level appId/appSecret.
1907
+ * Multi-agent shape (`accounts` present) belongs to `feishu_default_account`.
1908
+ */
1909
+ let FeishuChannelRule = class FeishuChannelRule extends DiagnoseRule {
1910
+ validate(ctx) {
1911
+ const feishu = getNestedMap(ctx.config, "channels", "feishu");
1912
+ if (!feishu) return {
1913
+ pass: false,
1914
+ message: "channels.feishu not found"
1915
+ };
1916
+ if (feishu.enabled !== true) return {
1917
+ pass: false,
1918
+ message: "channels.feishu.enabled mismatch: got " + feishu.enabled + ", expected true"
1919
+ };
1920
+ if (asRecord(feishu.accounts)) return { pass: true };
1921
+ if (feishu.appId !== ctx.vars.feishuAppID) return {
1922
+ pass: false,
1923
+ message: `channels.feishu.appId mismatch: got ${feishu.appId}, expected ${ctx.vars.feishuAppID}`
1924
+ };
1925
+ const secret = feishu.appSecret;
1926
+ if (typeof secret === "object" && secret !== null && !Array.isArray(secret)) {
1927
+ if (!matchMap(secret, DEFAULT_FEISHU_APP_SECRET)) return {
1928
+ pass: false,
1929
+ message: `channels.feishu.appSecret object mismatch: got ${JSON.stringify(secret)}`
1930
+ };
1931
+ } else if (typeof secret === "string") {
1932
+ if (secret !== ctx.vars.feishuAppSecret) return {
1933
+ pass: false,
1934
+ message: "channels.feishu.appSecret string value mismatch"
1935
+ };
1936
+ } else return {
1937
+ pass: false,
1938
+ message: "channels.feishu.appSecret has unexpected type"
1939
+ };
1940
+ return { pass: true };
1941
+ }
1942
+ repair(ctx) {
1943
+ setNestedValue(ctx.config, [
1944
+ "channels",
1945
+ "feishu",
1946
+ "enabled"
1947
+ ], true);
1948
+ if (asRecord(getNestedMap(ctx.config, "channels", "feishu").accounts)) return;
1949
+ setNestedValue(ctx.config, [
1950
+ "channels",
1951
+ "feishu",
1952
+ "appId"
1953
+ ], ctx.vars.feishuAppID);
1954
+ setNestedValue(ctx.config, [
1955
+ "channels",
1956
+ "feishu",
1957
+ "appSecret"
1958
+ ], DEFAULT_FEISHU_APP_SECRET);
1959
+ }
1960
+ };
1961
+ FeishuChannelRule = __decorate([Rule({
1962
+ key: "feishu_channel",
1963
+ description: "确保 channels.feishu 已启用,且包含单 agent 所需的 appId 和 appSecret 字段",
1964
+ dependsOn: ["config_syntax_check", "feishu_default_account"],
1965
+ repairMode: "standard",
1966
+ level: "critical",
1967
+ usesVars: ["feishuAppID", "feishuAppSecret"]
1968
+ })], FeishuChannelRule);
1969
+ //#endregion
2045
1970
  //#region src/rules/feishu-bot-id.ts
2046
1971
  let FeishuBotIdRule = class FeishuBotIdRule extends DiagnoseRule {
2047
1972
  validate(ctx) {
@@ -2087,229 +2012,267 @@ function appIdFromAccountId(accountId) {
2087
2012
  return accountId.slice(4);
2088
2013
  }
2089
2014
  //#endregion
2090
- //#region src/rules/session-persistence.ts
2091
- const DEFAULT_SESSION = {
2092
- resetByChannel: { feishu: {
2093
- mode: "idle",
2094
- idleMinutes: 10080
2095
- } },
2096
- maintenance: {
2097
- mode: "enforce",
2098
- resetArchiveRetention: "7d",
2099
- maxDiskBytes: "5GB"
2100
- }
2101
- };
2102
- let SessionPersistenceRule = class SessionPersistenceRule extends DiagnoseRule {
2015
+ //#region src/rules/feishu-accounts-consistency.ts
2016
+ /**
2017
+ * Detects count/id mismatches in multi-agent feishu channel config.
2018
+ * Single-agent configs (no `accounts`) are out of scope — handled by `feishu_channel`.
2019
+ *
2020
+ * Checks:
2021
+ * 1. channels.feishu.accounts count == agents.list count
2022
+ * 2. channels.feishu.accounts count == feishu bindings count
2023
+ * 3. Main agent's bot account appId == ctx.vars.feishuAppID
2024
+ * 4. agents.list id set == bindings agentId set (feishu channel)
2025
+ * 5. bindings accountId set (feishu channel) == accounts key set
2026
+ */
2027
+ let FeishuAccountsConsistencyRule = class FeishuAccountsConsistencyRule extends DiagnoseRule {
2103
2028
  validate(ctx) {
2104
- const session = getNestedMap(ctx.config, "session");
2029
+ const feishu = getNestedMap(ctx.config, "channels", "feishu");
2030
+ if (!feishu) return { pass: true };
2031
+ const accounts = asRecord(feishu.accounts);
2032
+ if (!accounts) return { pass: true };
2033
+ const accountCount = Object.keys(accounts).length;
2034
+ if (accountCount === 0) return { pass: true };
2105
2035
  const issues = [];
2106
- if (!getNestedMap(session, "resetByChannel")) issues.push("session.resetByChannel missing");
2107
- if (!getNestedMap(session, "maintenance")) issues.push("session.maintenance missing");
2036
+ const agentList = getAgentList(ctx.config);
2037
+ const feishuBindings = getFeishuBindings(ctx.config);
2038
+ if (agentList.length > 0 && accountCount !== agentList.length) issues.push(`飞书账号数(${accountCount})与 agents.list 代理数(${agentList.length})不一致,两者应相等`);
2039
+ if (feishuBindings.length > 0 && accountCount !== feishuBindings.length) issues.push(`飞书账号数(${accountCount})与 feishu 类型 bindings 数(${feishuBindings.length})不一致,两者应相等`);
2040
+ const mainAgent = findMainAgent(ctx.config);
2041
+ const mainId = typeof mainAgent?.id === "string" && mainAgent.id !== "" ? mainAgent.id : feishuBindings.find((b) => asRecord(b)?.agentId === "main") ? "main" : "";
2042
+ if (mainId === "") issues.push("存在多账号配置(channels.feishu.accounts)但无法确定主 agent:agents.list 中没有标记 default:true 或 id:\"main\" 的代理,feishu bindings 中也没有 agentId:\"main\" 的路由");
2043
+ else {
2044
+ const expectedAppId = ctx.vars?.feishuAppID;
2045
+ if (typeof expectedAppId === "string" && expectedAppId !== "") {
2046
+ const mainBinding = feishuBindings.find((b) => asRecord(b)?.agentId === mainId);
2047
+ const mainAccountId = asRecord(asRecord(mainBinding)?.match)?.accountId;
2048
+ if (typeof mainAccountId === "string") {
2049
+ const mainBotAcc = asRecord(accounts[mainAccountId]);
2050
+ if (mainBotAcc && mainBotAcc.appId !== expectedAppId) issues.push(`主 agent(${mainId})绑定的飞书账号(${mainAccountId})的 appId 为 "${mainBotAcc.appId}",与平台下发的 feishuAppID "${expectedAppId}" 不符`);
2051
+ } else if (mainBinding != null) issues.push(`主 agent(${mainId})的 feishu binding 缺少 match.accountId 字段,无法校验 appId 是否与平台下发的 feishuAppID "${expectedAppId}" 一致`);
2052
+ }
2053
+ }
2054
+ if (agentList.length > 0 && feishuBindings.length > 0) {
2055
+ const agentIds = new Set(agentList.map((a) => asRecord(a)?.id).filter((id) => typeof id === "string"));
2056
+ const bindingAgentIds = new Set(feishuBindings.map((b) => asRecord(b)?.agentId).filter((id) => typeof id === "string"));
2057
+ const missingInBindings = [...agentIds].filter((id) => !bindingAgentIds.has(id));
2058
+ const extraInBindings = [...bindingAgentIds].filter((id) => !agentIds.has(id));
2059
+ if (missingInBindings.length > 0) issues.push(`agent(${missingInBindings.join("、")})存在于 agents.list 但缺少对应的 feishu binding`);
2060
+ if (extraInBindings.length > 0) issues.push(`binding 引用了 agentId(${extraInBindings.join("、")}),但该 agent 不在 agents.list 中`);
2061
+ }
2062
+ if (feishuBindings.length > 0) {
2063
+ const accountIds = new Set(Object.keys(accounts));
2064
+ const bindingAccountIds = new Set(feishuBindings.map((b) => asRecord(asRecord(b)?.match)?.accountId).filter((id) => typeof id === "string"));
2065
+ const missingInBindings = [...accountIds].filter((id) => !bindingAccountIds.has(id));
2066
+ const extraInBindings = [...bindingAccountIds].filter((id) => !accountIds.has(id));
2067
+ if (missingInBindings.length > 0) issues.push(`飞书账号(${missingInBindings.join("、")})存在于 channels.feishu.accounts 但没有对应的 feishu binding`);
2068
+ if (extraInBindings.length > 0) issues.push(`binding 引用了 accountId(${extraInBindings.join("、")}),但该账号不在 channels.feishu.accounts 中`);
2069
+ }
2108
2070
  if (issues.length === 0) return { pass: true };
2109
2071
  return {
2110
2072
  pass: false,
2111
- message: issues.join("; ")
2073
+ message: issues.map((s, i) => `[${i + 1}] ${s}`).join("")
2112
2074
  };
2113
2075
  }
2114
- repair(ctx) {
2115
- const session = getNestedMap(ctx.config, "session") ?? {};
2116
- if (!getNestedMap(ctx.config, "session")) ctx.config.session = session;
2117
- const needsResetByChannel = !getNestedMap(session, "resetByChannel");
2118
- const needsMaintenance = !getNestedMap(session, "maintenance");
2119
- if (!needsResetByChannel && !needsMaintenance) return;
2120
- if (needsResetByChannel) session.resetByChannel = DEFAULT_SESSION.resetByChannel;
2121
- if (needsMaintenance) session.maintenance = DEFAULT_SESSION.maintenance;
2122
- }
2123
2076
  };
2124
- SessionPersistenceRule = __decorate([Rule({
2125
- key: "session_persistence",
2126
- description: "补齐 session.resetByChannelsession.maintenance 持久化配置",
2077
+ FeishuAccountsConsistencyRule = __decorate([Rule({
2078
+ key: "feishu_accounts_consistency",
2079
+ description: "检测多 agent 配置中 channels.feishu.accounts、agents.listfeishu bindings 之间的数量/ID 不一致(实验性)",
2127
2080
  dependsOn: ["config_syntax_check"],
2128
- repairMode: "standard",
2129
- level: "silent"
2130
- })], SessionPersistenceRule);
2081
+ repairMode: "check-only",
2082
+ level: "silent",
2083
+ usesVars: ["feishuAppID"],
2084
+ profile: "experimental"
2085
+ })], FeishuAccountsConsistencyRule);
2086
+ function getAgentList(config) {
2087
+ const agents = getNestedMap(config, "agents");
2088
+ return Array.isArray(agents?.list) ? agents.list : [];
2089
+ }
2090
+ function getFeishuBindings(config) {
2091
+ if (!Array.isArray(config.bindings)) return [];
2092
+ return config.bindings.filter((b) => {
2093
+ return asRecord(asRecord(b)?.match)?.channel === "feishu";
2094
+ });
2095
+ }
2131
2096
  //#endregion
2132
- //#region src/rules/gateway.ts
2133
- const DEFAULT_PORT = 18789;
2134
- const DEFAULT_MODE = "local";
2135
- const DEFAULT_BIND = "loopback";
2136
- const DEFAULT_AUTH_MODE = "token";
2137
- const DEFAULT_AUTH_TOKEN = {
2097
+ //#region src/rules/model-provider.ts
2098
+ const DEFAULT_API_KEY = {
2099
+ source: "file",
2100
+ provider: "miaoda-provider",
2101
+ id: "value"
2102
+ };
2103
+ const DEFAULT_X_API_KEY_HEADER = {
2138
2104
  source: "file",
2139
2105
  provider: "miaoda-secret-provider",
2140
- id: "/gateway_auth_token"
2106
+ id: "/models_providers_miaoda_headers_x_api_key"
2141
2107
  };
2142
- /** Required entries in gateway.trustedProxies. Repair appends any missing
2143
- * entries while preserving caller-added extras (no overwrite). */
2144
- const DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
2145
- let GatewayRule = class GatewayRule extends DiagnoseRule {
2108
+ const DEFAULT_API = "openai-completions";
2109
+ function getExpected(vars) {
2110
+ return {
2111
+ baseUrl: vars.baseURL + "/api/v1/sgw/model/proxy",
2112
+ apiKey: DEFAULT_API_KEY,
2113
+ api: DEFAULT_API,
2114
+ headers: { "x-api-key": DEFAULT_X_API_KEY_HEADER }
2115
+ };
2116
+ }
2117
+ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
2146
2118
  validate(ctx) {
2147
- const gateway = ctx.config.gateway;
2148
- if (!gateway || typeof gateway !== "object") return {
2149
- pass: false,
2150
- message: "gateway not found"
2151
- };
2152
- const gw = gateway;
2153
- if (gw.port !== DEFAULT_PORT) return {
2119
+ const provider = getNestedMap(ctx.config, "models", "providers", "miaoda");
2120
+ if (!provider) return {
2154
2121
  pass: false,
2155
- message: "gateway.port mismatch: got " + gw.port + ", expected 18789"
2122
+ message: "models.providers.miaoda not found"
2156
2123
  };
2157
- if (gw.mode !== DEFAULT_MODE) return {
2124
+ const expected = getExpected(ctx.vars);
2125
+ if (provider.baseUrl !== expected.baseUrl) return {
2158
2126
  pass: false,
2159
- message: "gateway.mode mismatch: got " + gw.mode + ", expected local"
2127
+ message: "baseUrl mismatch: got " + provider.baseUrl + ", expected " + expected.baseUrl
2160
2128
  };
2161
- if (gw.bind !== DEFAULT_BIND) return {
2162
- pass: false,
2163
- message: "gateway.bind mismatch: got " + gw.bind + ", expected loopback"
2164
- };
2165
- const auth = gw.auth;
2166
- if (!auth || typeof auth !== "object") return {
2167
- pass: false,
2168
- message: "gateway.auth not found"
2169
- };
2170
- const authObj = auth;
2171
- if (authObj.mode !== DEFAULT_AUTH_MODE) return {
2172
- pass: false,
2173
- message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected token"
2174
- };
2175
- const token = authObj.token;
2176
- if (typeof token === "string") {
2177
- if (token !== ctx.vars.gatewayToken) return {
2129
+ const expectedApiKey = expected.apiKey && typeof expected.apiKey === "object" ? expected.apiKey : null;
2130
+ if (expectedApiKey) if (typeof provider.apiKey === "object" && provider.apiKey !== null && !Array.isArray(provider.apiKey)) {
2131
+ if (!matchMap(provider.apiKey, expectedApiKey)) return {
2178
2132
  pass: false,
2179
- message: "gateway.auth.token string value mismatch"
2133
+ message: "apiKey object mismatch: got " + JSON.stringify(provider.apiKey)
2180
2134
  };
2181
- } else if (typeof token === "object" && token !== null && !Array.isArray(token)) {
2182
- if (!matchMap(token, DEFAULT_AUTH_TOKEN)) return {
2135
+ } else if (typeof provider.apiKey === "string") {
2136
+ if (!isValidJWT(provider.apiKey)) return {
2183
2137
  pass: false,
2184
- message: "gateway.auth.token object mismatch: got " + JSON.stringify(token)
2138
+ message: "apiKey is a string but not a valid/unexpired JWT"
2185
2139
  };
2186
2140
  } else return {
2187
2141
  pass: false,
2188
- message: "gateway.auth.token has unexpected type"
2189
- };
2190
- const controlUi = gw.controlUi;
2191
- if (!controlUi || typeof controlUi !== "object") return {
2192
- pass: false,
2193
- message: "gateway.controlUi not found"
2194
- };
2195
- if (controlUi.dangerouslyDisableDeviceAuth !== true) return {
2196
- pass: false,
2197
- message: "gateway.controlUi.dangerouslyDisableDeviceAuth must be true, got " + controlUi.dangerouslyDisableDeviceAuth
2198
- };
2199
- const proxies = gw.trustedProxies;
2200
- if (!Array.isArray(proxies)) return {
2201
- pass: false,
2202
- message: "gateway.trustedProxies missing or not an array"
2203
- };
2204
- const missing = DEFAULT_TRUSTED_PROXIES.filter((p) => !proxies.includes(p));
2205
- if (missing.length > 0) return {
2206
- pass: false,
2207
- message: "gateway.trustedProxies missing: " + JSON.stringify(missing)
2142
+ message: "apiKey has unexpected type"
2208
2143
  };
2144
+ if (expected.api !== void 0) {
2145
+ if (provider.api !== expected.api) return {
2146
+ pass: false,
2147
+ message: "api mismatch: got " + provider.api + ", expected " + expected.api
2148
+ };
2149
+ }
2150
+ const expectedHeaders = getNestedMap(expected, "headers");
2151
+ const expectedXApiKey = expectedHeaders ? expectedHeaders["x-api-key"] : void 0;
2152
+ if (expectedXApiKey && typeof expectedXApiKey === "object") {
2153
+ const xKey = (typeof provider.headers === "object" && provider.headers || {})["x-api-key"];
2154
+ if (typeof xKey === "object" && xKey !== null && !Array.isArray(xKey)) {
2155
+ if (!matchMap(xKey, expectedXApiKey)) return {
2156
+ pass: false,
2157
+ message: "headers.x-api-key object mismatch: got " + JSON.stringify(xKey)
2158
+ };
2159
+ } else if (typeof xKey === "string") {
2160
+ if (xKey !== ctx.vars.innerAPIKey) return {
2161
+ pass: false,
2162
+ message: "headers.x-api-key string value mismatch"
2163
+ };
2164
+ } else return {
2165
+ pass: false,
2166
+ message: "headers.x-api-key has unexpected type"
2167
+ };
2168
+ }
2209
2169
  return { pass: true };
2210
2170
  }
2211
2171
  repair(ctx) {
2212
- setNestedValue(ctx.config, ["gateway", "port"], DEFAULT_PORT);
2213
- setNestedValue(ctx.config, ["gateway", "mode"], DEFAULT_MODE);
2214
- setNestedValue(ctx.config, ["gateway", "bind"], DEFAULT_BIND);
2215
- setNestedValue(ctx.config, [
2216
- "gateway",
2217
- "auth",
2218
- "mode"
2219
- ], DEFAULT_AUTH_MODE);
2220
- setNestedValue(ctx.config, [
2221
- "gateway",
2222
- "auth",
2223
- "token"
2224
- ], DEFAULT_AUTH_TOKEN);
2172
+ const expected = getExpected(ctx.vars);
2225
2173
  setNestedValue(ctx.config, [
2226
- "gateway",
2227
- "controlUi",
2228
- "dangerouslyDisableDeviceAuth"
2229
- ], true);
2230
- const gw = ctx.config.gateway ?? {};
2231
- const current = Array.isArray(gw.trustedProxies) ? gw.trustedProxies.slice() : [];
2232
- const seen = new Set(current.map((v) => String(v)));
2233
- for (const p of DEFAULT_TRUSTED_PROXIES) if (!seen.has(p)) {
2234
- current.push(p);
2235
- seen.add(p);
2236
- }
2237
- setNestedValue(ctx.config, ["gateway", "trustedProxies"], current);
2174
+ "models",
2175
+ "providers",
2176
+ "miaoda",
2177
+ "baseUrl"
2178
+ ], expected.baseUrl);
2179
+ if (expected.apiKey !== void 0) setNestedValue(ctx.config, [
2180
+ "models",
2181
+ "providers",
2182
+ "miaoda",
2183
+ "apiKey"
2184
+ ], expected.apiKey);
2185
+ if (expected.api !== void 0) setNestedValue(ctx.config, [
2186
+ "models",
2187
+ "providers",
2188
+ "miaoda",
2189
+ "api"
2190
+ ], expected.api);
2191
+ if (expected.headers !== void 0) setNestedValue(ctx.config, [
2192
+ "models",
2193
+ "providers",
2194
+ "miaoda",
2195
+ "headers"
2196
+ ], expected.headers);
2238
2197
  }
2239
2198
  };
2240
- GatewayRule = __decorate([Rule({
2241
- key: "gateway",
2242
- description: "验证 gateway 必填字段(portmode、bind、auth、trustedProxies)是否存在且有效;修复缺失或非法值",
2199
+ ModelProviderRule = __decorate([Rule({
2200
+ key: "model_provider",
2201
+ description: "检查妙搭 model provider 配置项(baseUrlapiKey、必要 headers)是否存在且格式正确;非妙搭沙箱时跳过",
2243
2202
  dependsOn: ["config_syntax_check"],
2244
2203
  repairMode: "standard",
2245
2204
  level: "critical",
2246
- usesVars: ["gatewayToken"]
2247
- })], GatewayRule);
2205
+ usesVars: ["innerAPIKey", "baseURL"],
2206
+ skipWhen: ({ hasMiaoda }) => !hasMiaoda
2207
+ })], ModelProviderRule);
2248
2208
  //#endregion
2249
- //#region src/rules/allowed-origins.ts
2250
- let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
2209
+ //#region src/rules/secret-provider.ts
2210
+ var _SecretProviderRule;
2211
+ let SecretProviderRule = class SecretProviderRule extends DiagnoseRule {
2212
+ static {
2213
+ _SecretProviderRule = this;
2214
+ }
2215
+ static DEFAULT_MIAODA_PROVIDER = {
2216
+ source: "file",
2217
+ path: "/home/gem/workspace/.force/openclaw/miaoda-provider-key",
2218
+ mode: "singleValue"
2219
+ };
2220
+ static DEFAULT_MIAODA_SECRET_PROVIDER = {
2221
+ source: "file",
2222
+ path: "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json",
2223
+ mode: "json"
2224
+ };
2251
2225
  validate(ctx) {
2252
- const expected = getExpectedOrigins(ctx.vars);
2253
- if (expected.length === 0) return { pass: true };
2254
- const current = getCurrentOrigins(ctx.config);
2255
- if (hasWildcard(current)) return { pass: true };
2256
- const missing = findMissing(current, expected);
2257
- if (missing.length === 0) return { pass: true };
2258
- return {
2226
+ const providers = getNestedMap(ctx.config, "secrets", "providers");
2227
+ if (!providers) return {
2259
2228
  pass: false,
2260
- message: "allowedOrigins missing: " + JSON.stringify(missing)
2229
+ message: "secrets.providers not found"
2261
2230
  };
2231
+ const { mp: expectedMP, msp: expectedMSP } = this.getExpected();
2232
+ if (ctx.providerDeps.usesMiaodaProvider) {
2233
+ const mp = providers["miaoda-provider"];
2234
+ if (!mp || typeof mp !== "object" || !matchMap(mp, expectedMP)) return {
2235
+ pass: false,
2236
+ message: "secrets.providers.miaoda-provider mismatch: got " + JSON.stringify(mp)
2237
+ };
2238
+ }
2239
+ if (ctx.providerDeps.usesMiaodaSecretProvider) {
2240
+ const msp = providers["miaoda-secret-provider"];
2241
+ if (!msp || typeof msp !== "object" || !matchMap(msp, expectedMSP)) return {
2242
+ pass: false,
2243
+ message: "secrets.providers.miaoda-secret-provider mismatch: got " + JSON.stringify(msp)
2244
+ };
2245
+ }
2246
+ return { pass: true };
2262
2247
  }
2263
2248
  repair(ctx) {
2264
- const expected = getExpectedOrigins(ctx.vars);
2265
- const current = getCurrentOrigins(ctx.config);
2266
- if (hasWildcard(current)) return;
2267
- const missing = findMissing(current, expected);
2268
- if (missing.length > 0) {
2269
- const seen = /* @__PURE__ */ new Set();
2270
- const merged = [];
2271
- for (const o of current) if (!seen.has(o)) {
2272
- merged.push(o);
2273
- seen.add(o);
2274
- }
2275
- for (const o of missing) if (!seen.has(o)) {
2276
- merged.push(o);
2277
- seen.add(o);
2278
- }
2279
- setNestedValue(ctx.config, [
2280
- "gateway",
2281
- "controlUi",
2282
- "allowedOrigins"
2283
- ], merged);
2284
- }
2249
+ const { mp: expectedMP, msp: expectedMSP } = this.getExpected();
2250
+ if (ctx.providerDeps.usesMiaodaProvider) setNestedValue(ctx.config, [
2251
+ "secrets",
2252
+ "providers",
2253
+ "miaoda-provider"
2254
+ ], expectedMP);
2255
+ if (ctx.providerDeps.usesMiaodaSecretProvider) setNestedValue(ctx.config, [
2256
+ "secrets",
2257
+ "providers",
2258
+ "miaoda-secret-provider"
2259
+ ], expectedMSP);
2260
+ }
2261
+ getExpected() {
2262
+ return {
2263
+ mp: _SecretProviderRule.DEFAULT_MIAODA_PROVIDER,
2264
+ msp: _SecretProviderRule.DEFAULT_MIAODA_SECRET_PROVIDER
2265
+ };
2285
2266
  }
2286
2267
  };
2287
- AllowedOriginsRule = __decorate([Rule({
2288
- key: "allowed_origins",
2289
- description: "确保所有 expectedOrigins 条目都存在于 gateway.auth.allowedOrigins 中;自动追加缺失的条目",
2268
+ SecretProviderRule = _SecretProviderRule = __decorate([Rule({
2269
+ key: "secret_provider",
2270
+ description: "验证 miaoda-provider / miaoda-secret-provider 配置块是否存在且完整;未检测到妙搭 provider 时跳过",
2290
2271
  dependsOn: ["config_syntax_check"],
2291
2272
  repairMode: "standard",
2292
2273
  level: "critical",
2293
- usesVars: ["expectedOrigins"]
2294
- })], AllowedOriginsRule);
2295
- function getExpectedOrigins(vars) {
2296
- return Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
2297
- }
2298
- function getCurrentOrigins(config) {
2299
- const controlUi = getNestedMap(config, "gateway", "controlUi");
2300
- if (!controlUi) return [];
2301
- const raw = controlUi.allowedOrigins;
2302
- if (!Array.isArray(raw)) return [];
2303
- return raw.filter((o) => typeof o === "string");
2304
- }
2305
- function findMissing(current, expected) {
2306
- const set = new Set(current);
2307
- return expected.filter((o) => !set.has(o));
2308
- }
2309
- /** Exact "*" entry means allow-all; pattern globs like "https://*.example.com" don't count. */
2310
- function hasWildcard(origins) {
2311
- return origins.includes("*");
2312
- }
2274
+ skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider && !deps.usesMiaodaSecretProvider
2275
+ })], SecretProviderRule);
2313
2276
  //#endregion
2314
2277
  //#region src/rules/jwt-token.ts
2315
2278
  let JwtTokenRule = class JwtTokenRule extends DiagnoseRule {
@@ -2438,54 +2401,124 @@ SecretsFileRule = __decorate([Rule({
2438
2401
  skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
2439
2402
  })], SecretsFileRule);
2440
2403
  //#endregion
2441
- //#region src/rules/cleanup-install-backup-dirs.ts
2442
- const DIR_PREFIX = ".openclaw-install-";
2443
- function resolveExtensionsDir(configPath) {
2444
- return node_path.default.join(node_path.default.dirname(configPath), "extensions");
2445
- }
2446
- function findLeftoverDirs(extensionsDir) {
2447
- if (!fileExists(extensionsDir)) return [];
2448
- let entries;
2449
- try {
2450
- entries = node_fs.default.readdirSync(extensionsDir, { withFileTypes: true });
2451
- } catch {
2452
- return [];
2453
- }
2454
- return entries.filter((e) => e.isDirectory() && e.name.startsWith(DIR_PREFIX)).map((e) => node_path.default.join(extensionsDir, e.name));
2404
+ //#region src/rules/agents-md-service-commands.ts
2405
+ const SERVICE_COMMAND_REPLACEMENTS = [
2406
+ ["`sh scripts/start.sh`", "`bash /opt/force/bin/openclaw_scripts/start.sh`"],
2407
+ ["`sh scripts/restart.sh`", "`bash /opt/force/bin/openclaw_scripts/restart.sh`"],
2408
+ ["`sh scripts/stop.sh`", "`bash /opt/force/bin/openclaw_scripts/stop.sh`"]
2409
+ ];
2410
+ let AgentsMdServiceCommandsRule = class AgentsMdServiceCommandsRule extends DiagnoseRule {
2411
+ validate(ctx) {
2412
+ const agentsMdPath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
2413
+ return hasOldServiceCommands(node_fs.default.readFileSync(filePath, "utf-8"));
2414
+ });
2415
+ if (!agentsMdPath) return { pass: true };
2416
+ return {
2417
+ pass: false,
2418
+ message: `${agentsMdPath} 中存在旧版服务命令,需要改为 /opt/force/bin/openclaw_scripts 路径`
2419
+ };
2420
+ }
2421
+ repair(ctx) {
2422
+ for (const agentsMdPath of collectExistingAgentsMdPaths(ctx)) {
2423
+ const content = node_fs.default.readFileSync(agentsMdPath, "utf-8");
2424
+ const next = replaceOldServiceCommands(content);
2425
+ if (next !== content) node_fs.default.writeFileSync(agentsMdPath, next, "utf-8");
2426
+ }
2427
+ }
2428
+ };
2429
+ AgentsMdServiceCommandsRule = __decorate([Rule({
2430
+ key: "agents_md_service_commands",
2431
+ description: "检测各智能体 AGENTS.md 中旧版 OpenClaw 服务命令,并替换为 /opt/force/bin/openclaw_scripts 路径",
2432
+ dependsOn: ["config_syntax_check"],
2433
+ repairMode: "standard",
2434
+ level: "silent"
2435
+ })], AgentsMdServiceCommandsRule);
2436
+ function hasOldServiceCommands(content) {
2437
+ return SERVICE_COMMAND_REPLACEMENTS.some(([oldCmd]) => content.includes(oldCmd));
2455
2438
  }
2456
- let CleanupInstallBackupDirsRule = class CleanupInstallBackupDirsRule extends DiagnoseRule {
2439
+ function replaceOldServiceCommands(content) {
2440
+ let next = content;
2441
+ for (const [oldCmd, newCmd] of SERVICE_COMMAND_REPLACEMENTS) next = next.split(oldCmd).join(newCmd);
2442
+ return next;
2443
+ }
2444
+ //#endregion
2445
+ //#region src/rules/agents-md-resource-constrained-tools.ts
2446
+ const RESOURCE_CONSTRAINED_TOOLS_TAG = "resource_constrained_tools";
2447
+ const RESOURCE_CONSTRAINED_TOOLS_BLOCK = [
2448
+ "<resource_constrained_tools>",
2449
+ "### Browser tab 管理",
2450
+ "Chrome tab 是沙箱里的**头号资源杀手**:在线上 OpenClaw 集群的 Mem Top10 里占 42%、CPU Top10 里占 70%。下面规则**全部**指 `browser` 工具自身的 action(不是别的 MCP),按\"调用前 / 调用后 / 自查 / 兜底\"四阶段顺序遵守,不要跳。",
2451
+ "#### 调用 `browser action: \"open\"` 之前——必答两问",
2452
+ "1. 我上一次 `open` 的 tab 是否还活着?活着 → 用 `action: \"navigate\"` + 它的 `targetId` 换 URL,**不要新开**。",
2453
+ "2. 完成这次访问后,我下一步能不能立刻 `close`?不能 → 拆小任务,先不开。",
2454
+ "#### 调用 `browser action: \"open\"` 之后——下一个 `browser` 工具调用必须是以下之一",
2455
+ "- `action: \"navigate\"`(带 open 返回的 `targetId`):复用此 tab 换 URL。",
2456
+ "- `action: \"close\"`(带 open 返回的 `targetId`):关闭此 tab,任务结束。",
2457
+ "- `action: \"snapshot\"` / `\"click\"` / `\"type\"` 等:仅当**当前任务仍在这个 tab 上推进**时可用;任务做完仍必须显式 `close` 并传 `targetId`。",
2458
+ "**严禁**下面这种序列(线上实测到的 chrome 内存 Top1 故障模式):",
2459
+ " BAD:",
2460
+ " browser(open, url=A) → snapshot",
2461
+ " → browser(open, url=B) → snapshot",
2462
+ " → browser(open, url=C) → snapshot",
2463
+ " → …(任务结束未关)",
2464
+ " # 每次 open 都新建一个 tab;snapshot 不会关;chrome 堆栈式泄漏。",
2465
+ "应该写成:",
2466
+ " GOOD:",
2467
+ " browser(open, url=A) → snapshot",
2468
+ " → browser(navigate, targetId=t1, url=B) → snapshot",
2469
+ " → … → browser(close, targetId=t1)",
2470
+ " # 始终复用同一个 tab;任务结束显式 close。",
2471
+ "#### 异常时止损",
2472
+ "发现 Chrome 卡顿、心跳延迟、沙箱负载升高:先 `close` 所有非活跃 tab;仍不行就让 shell 杀掉 chrome 进程(`pkill -INT chromium-browser` 或等价命令)让它按需重启,**不要**继续开新 tab。",
2473
+ "### FFmpeg / 媒体编码使用限制",
2474
+ "ffmpeg 默认吃满所有 CPU 核心,沙箱只有 1–2 vCPU,一次无约束转码就能拖垮心跳和 Browser。下面的规则**只针对 shell 直接调用的 `ffmpeg` / `ffprobe`**:",
2475
+ "- **优先用 skill:** `skills/video-frames` 等媒体 skill 是已审查过的窄用例,跟着它们的写法走比自己手撸 ffmpeg 安全;只在 skill 不覆盖时才直接 shell `ffmpeg`。注意 `video-frames` 抽中后段帧用 `--time`(demuxer seek,几乎零 CPU)而非 `--index N`(会从头解码 N 帧)。",
2476
+ "- **先 probe 再 encode:** 转码前先 `ffprobe -v error -threads 1 -show_streams -show_format -of json <input>` 拿到时长、码率、编码;能用 `-c copy` 流拷贝就绝不重编码;时长超 1200s 先 `-t` 截断或拒绝任务。",
2477
+ "- **强制限线程:** 必须显式带 `-threads 1`(最多 `-threads 2`),滤镜链追加 `-filter_threads 1 -filter_complex_threads 1`。",
2478
+ "- **软件编码必须 ultrafast:** x264/x265 统一 `-preset ultrafast`,禁止 `medium` 及以上预设;质量不够时降分辨率(`-vf scale=-2:720`)或降帧率(`-r 24`),不要靠提高 preset。",
2479
+ "- **禁止并发 ffmpeg:** 同一沙箱内同时只允许一个 ffmpeg 进程,开下一个前用 `pgrep -x ffmpeg` 确认无残留;禁止 `xargs -P`、禁止循环里并行批处理。",
2480
+ "- **必须前台 + 超时兜底:** 用 `timeout 45 ffmpeg -nostdin -hide_banner -loglevel error ...` 包一层(与上游 `MEDIA_FFMPEG_TIMEOUT_MS` 对齐,最多放宽到 `timeout 90`),ffprobe 用 `timeout 10`;禁止 `&` 后台、禁止 `nohup`,跑飞时必须能 Ctrl-C 打断。",
2481
+ "- **中间产物即用即删:** 临时切片、调色板、`-pass 1` log 等,任务结束或失败时立即 `rm -f`,不要把 `/tmp/*.mp4`、`ffmpeg2pass-*.log` 留到下一个任务。",
2482
+ "- **异常时硬停:** 沙箱变慢、心跳丢失、或 `frame=` 长时间不前进,立即 `pkill -INT ffmpeg`(不行就 `-KILL`);**不要**重试相同命令,先降分辨率/preset 或改用流拷贝。",
2483
+ "</resource_constrained_tools>"
2484
+ ].join("\n");
2485
+ let AgentsMdResourceConstrainedToolsRule = class AgentsMdResourceConstrainedToolsRule extends DiagnoseRule {
2457
2486
  validate(ctx) {
2458
- const { configPath } = ctx;
2459
- if (!configPath) return { pass: true };
2460
- const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
2461
- if (dirs.length === 0) return { pass: true };
2487
+ const stalePath = collectExistingAgentsMdPaths(ctx).find((filePath) => {
2488
+ return !hasCurrentResourceConstrainedToolsBlock(node_fs.default.readFileSync(filePath, "utf-8"));
2489
+ });
2490
+ if (!stalePath) return { pass: true };
2462
2491
  return {
2463
2492
  pass: false,
2464
- message: `extensions 目录下发现 ${dirs.length} ${DIR_PREFIX}* 脏目录需要清理`
2493
+ message: `${stalePath} 中缺少或存在旧版 resource_constrained_tools,需要更新`
2465
2494
  };
2466
2495
  }
2467
2496
  repair(ctx) {
2468
- const { configPath } = ctx;
2469
- if (!configPath) return;
2470
- const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
2471
- const failures = [];
2472
- for (const dir of dirs) try {
2473
- node_fs.default.rmSync(dir, {
2474
- recursive: true,
2475
- force: true
2476
- });
2477
- } catch (e) {
2478
- failures.push(`${node_path.default.basename(dir)}: ${e.message}`);
2497
+ for (const filePath of collectExistingAgentsMdPaths(ctx)) {
2498
+ const content = node_fs.default.readFileSync(filePath, "utf-8");
2499
+ const next = upsertResourceConstrainedToolsBlock(content);
2500
+ if (next !== content) node_fs.default.writeFileSync(filePath, next, "utf-8");
2479
2501
  }
2480
- if (dirs.length > 0 && failures.length === dirs.length) throw new Error(`cleanup_install_backup_dirs: 全部清理失败: ${failures.join("; ")}`);
2481
2502
  }
2482
2503
  };
2483
- CleanupInstallBackupDirsRule = __decorate([Rule({
2484
- key: "cleanup_install_backup_dirs",
2485
- description: "清理 extensions/ 目录中升级中断遗留的 .openclaw-install-stage-* .openclaw-install-backups/ 脏目录",
2504
+ AgentsMdResourceConstrainedToolsRule = __decorate([Rule({
2505
+ key: "agents_md_resource_constrained_tools",
2506
+ description: "检测各智能体 AGENTS.md resource_constrained_tools 规则块,缺失时追加,存在时替换为当前内容",
2507
+ dependsOn: ["config_syntax_check"],
2486
2508
  repairMode: "standard",
2487
- level: "critical"
2488
- })], CleanupInstallBackupDirsRule);
2509
+ level: "silent"
2510
+ })], AgentsMdResourceConstrainedToolsRule);
2511
+ function hasCurrentResourceConstrainedToolsBlock(content) {
2512
+ return normalizeForTemplateMatch(content).includes(normalizeForTemplateMatch(RESOURCE_CONSTRAINED_TOOLS_BLOCK));
2513
+ }
2514
+ function normalizeForTemplateMatch(content) {
2515
+ return content.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).join("\n");
2516
+ }
2517
+ function upsertResourceConstrainedToolsBlock(content) {
2518
+ const tagRe = new RegExp(`<${RESOURCE_CONSTRAINED_TOOLS_TAG}>[\\s\\S]*?</${RESOURCE_CONSTRAINED_TOOLS_TAG}>`);
2519
+ if (tagRe.test(content)) return content.replace(tagRe, RESOURCE_CONSTRAINED_TOOLS_BLOCK);
2520
+ return `${content}${content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n"}${RESOURCE_CONSTRAINED_TOOLS_BLOCK}\n`;
2521
+ }
2489
2522
  //#endregion
2490
2523
  //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
2491
2524
  /**
@@ -2545,73 +2578,47 @@ MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
2545
2578
  level: "silent"
2546
2579
  })], MiaodaOfficialPluginsInstallSpecUnlockRule);
2547
2580
  //#endregion
2548
- //#region src/rules/old-miaoda-plugins-cleanup.ts
2549
- const NEW_MIAODA = "openclaw-extension-miaoda";
2550
- const OLD_PLUGIN_NAMES = Object.freeze([
2551
- "openclaw-feishu-greeting",
2552
- "openclaw-miaoda-keepalive",
2553
- "feishu-greeting",
2554
- "miaoda-keepalive"
2555
- ]);
2556
- function getPluginMaps(config) {
2557
- const rawAllow = asRecord(config.plugins)?.allow;
2558
- return {
2559
- entries: getNestedMap(config, "plugins", "entries"),
2560
- installs: getNestedMap(config, "plugins", "installs"),
2561
- allow: Array.isArray(rawAllow) ? rawAllow : void 0
2562
- };
2563
- }
2564
- function hasNewMiaoda({ entries, installs, allow }) {
2565
- return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
2566
- }
2567
- function findResiduals({ entries, installs, allow }, extensionsDir) {
2568
- return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
2569
- }
2570
- let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
2581
+ //#region src/rules/miaoda-plugin-allow.ts
2582
+ const MIAODA_PLUGIN = "openclaw-extension-miaoda";
2583
+ let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
2571
2584
  validate(ctx) {
2572
- const maps = getPluginMaps(ctx.config);
2573
- if (!hasNewMiaoda(maps)) return { pass: true };
2574
- const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
2575
- if (residuals.length === 0) return { pass: true };
2585
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return { pass: true };
2586
+ if (getAllow$1(ctx.config).includes(MIAODA_PLUGIN)) return { pass: true };
2576
2587
  return {
2577
2588
  pass: false,
2578
- message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
2589
+ message: `plugins.allow 缺少 ${MIAODA_PLUGIN}(已在 extensions/ 下装但未启用)`
2579
2590
  };
2580
2591
  }
2581
2592
  repair(ctx) {
2582
- const maps = getPluginMaps(ctx.config);
2583
- if (!hasNewMiaoda(maps)) return;
2584
- const extensionsDir = getExtensionsDir(ctx.configPath);
2585
- const { entries, installs, allow } = maps;
2586
- const oldSet = new Set(OLD_PLUGIN_NAMES);
2587
- if (allow) for (let i = allow.length - 1; i >= 0; i--) {
2588
- const v = allow[i];
2589
- if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
2590
- }
2591
- for (const name of OLD_PLUGIN_NAMES) {
2592
- if (entries && name in entries) delete entries[name];
2593
- if (installs && name in installs) delete installs[name];
2594
- const target = node_path.default.join(extensionsDir, name);
2595
- const rel = node_path.default.relative(extensionsDir, target);
2596
- if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
2597
- try {
2598
- node_fs.default.rmSync(target, {
2599
- recursive: true,
2600
- force: true
2601
- });
2602
- } catch (e) {
2603
- console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
2604
- }
2593
+ if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return;
2594
+ const plugins = ctx.config.plugins;
2595
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
2596
+ ctx.config.plugins = { allow: [MIAODA_PLUGIN] };
2597
+ return;
2605
2598
  }
2599
+ const pluginsMap = plugins;
2600
+ const rawAllow = pluginsMap.allow;
2601
+ const allow = Array.isArray(rawAllow) ? rawAllow : [];
2602
+ if (allow.includes(MIAODA_PLUGIN)) return;
2603
+ allow.push(MIAODA_PLUGIN);
2604
+ pluginsMap.allow = allow;
2606
2605
  }
2607
2606
  };
2608
- OldMiaodaPluginsCleanupRule = __decorate([Rule({
2609
- key: "old_miaoda_plugins_cleanup",
2610
- description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
2607
+ MiaodaPluginAllowRule = __decorate([Rule({
2608
+ key: "miaoda_plugin_allow",
2609
+ description: " openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
2611
2610
  dependsOn: ["config_syntax_check"],
2612
2611
  repairMode: "standard",
2613
- level: "silent"
2614
- })], OldMiaodaPluginsCleanupRule);
2612
+ level: "critical",
2613
+ profile: "standard"
2614
+ })], MiaodaPluginAllowRule);
2615
+ function getAllow$1(config) {
2616
+ const plugins = config.plugins;
2617
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2618
+ const allow = plugins.allow;
2619
+ if (!Array.isArray(allow)) return [];
2620
+ return allow.filter((e) => typeof e === "string");
2621
+ }
2615
2622
  //#endregion
2616
2623
  //#region src/rules/lark-plugin-allow.ts
2617
2624
  const LARK_PLUGIN = "openclaw-lark";
@@ -2619,7 +2626,7 @@ const LEGACY_LARK_PLUGIN = "feishu-openclaw-plugin";
2619
2626
  const LARK_PLUGIN_NAMES$1 = [LARK_PLUGIN, LEGACY_LARK_PLUGIN];
2620
2627
  let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
2621
2628
  validate(ctx) {
2622
- const allow = getAllow$1(ctx.config);
2629
+ const allow = getAllow(ctx.config);
2623
2630
  if (LARK_PLUGIN_NAMES$1.some((name) => allow.includes(name))) return { pass: true };
2624
2631
  const installed = detectInstalledLarkPlugin(getExtensionsDir(ctx.configPath));
2625
2632
  if (installed == null) return { pass: true };
@@ -2651,7 +2658,7 @@ LarkPluginAllowRule = __decorate([Rule({
2651
2658
  repairMode: "standard",
2652
2659
  level: "critical"
2653
2660
  })], LarkPluginAllowRule);
2654
- function getAllow$1(config) {
2661
+ function getAllow(config) {
2655
2662
  const plugins = config.plugins;
2656
2663
  if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2657
2664
  const allow = plugins.allow;
@@ -2675,47 +2682,73 @@ function pluginPackageJsonExists(extDir, pluginDir) {
2675
2682
  }
2676
2683
  }
2677
2684
  //#endregion
2678
- //#region src/rules/miaoda-plugin-allow.ts
2679
- const MIAODA_PLUGIN = "openclaw-extension-miaoda";
2680
- let MiaodaPluginAllowRule = class MiaodaPluginAllowRule extends DiagnoseRule {
2685
+ //#region src/rules/old-miaoda-plugins-cleanup.ts
2686
+ const NEW_MIAODA = "openclaw-extension-miaoda";
2687
+ const OLD_PLUGIN_NAMES = Object.freeze([
2688
+ "openclaw-feishu-greeting",
2689
+ "openclaw-miaoda-keepalive",
2690
+ "feishu-greeting",
2691
+ "miaoda-keepalive"
2692
+ ]);
2693
+ function getPluginMaps(config) {
2694
+ const rawAllow = asRecord(config.plugins)?.allow;
2695
+ return {
2696
+ entries: getNestedMap(config, "plugins", "entries"),
2697
+ installs: getNestedMap(config, "plugins", "installs"),
2698
+ allow: Array.isArray(rawAllow) ? rawAllow : void 0
2699
+ };
2700
+ }
2701
+ function hasNewMiaoda({ entries, installs, allow }) {
2702
+ return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
2703
+ }
2704
+ function findResiduals({ entries, installs, allow }, extensionsDir) {
2705
+ return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
2706
+ }
2707
+ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
2681
2708
  validate(ctx) {
2682
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return { pass: true };
2683
- if (getAllow(ctx.config).includes(MIAODA_PLUGIN)) return { pass: true };
2709
+ const maps = getPluginMaps(ctx.config);
2710
+ if (!hasNewMiaoda(maps)) return { pass: true };
2711
+ const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
2712
+ if (residuals.length === 0) return { pass: true };
2684
2713
  return {
2685
2714
  pass: false,
2686
- message: `plugins.allow 缺少 ${MIAODA_PLUGIN}(已在 extensions/ 下装但未启用)`
2715
+ message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
2687
2716
  };
2688
2717
  }
2689
2718
  repair(ctx) {
2690
- if (!isPluginInstalledOnDisk(getExtensionsDir(ctx.configPath), MIAODA_PLUGIN)) return;
2691
- const plugins = ctx.config.plugins;
2692
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) {
2693
- ctx.config.plugins = { allow: [MIAODA_PLUGIN] };
2694
- return;
2719
+ const maps = getPluginMaps(ctx.config);
2720
+ if (!hasNewMiaoda(maps)) return;
2721
+ const extensionsDir = getExtensionsDir(ctx.configPath);
2722
+ const { entries, installs, allow } = maps;
2723
+ const oldSet = new Set(OLD_PLUGIN_NAMES);
2724
+ if (allow) for (let i = allow.length - 1; i >= 0; i--) {
2725
+ const v = allow[i];
2726
+ if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
2727
+ }
2728
+ for (const name of OLD_PLUGIN_NAMES) {
2729
+ if (entries && name in entries) delete entries[name];
2730
+ if (installs && name in installs) delete installs[name];
2731
+ const target = node_path.default.join(extensionsDir, name);
2732
+ const rel = node_path.default.relative(extensionsDir, target);
2733
+ if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
2734
+ try {
2735
+ node_fs.default.rmSync(target, {
2736
+ recursive: true,
2737
+ force: true
2738
+ });
2739
+ } catch (e) {
2740
+ console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
2741
+ }
2695
2742
  }
2696
- const pluginsMap = plugins;
2697
- const rawAllow = pluginsMap.allow;
2698
- const allow = Array.isArray(rawAllow) ? rawAllow : [];
2699
- if (allow.includes(MIAODA_PLUGIN)) return;
2700
- allow.push(MIAODA_PLUGIN);
2701
- pluginsMap.allow = allow;
2702
2743
  }
2703
2744
  };
2704
- MiaodaPluginAllowRule = __decorate([Rule({
2705
- key: "miaoda_plugin_allow",
2706
- description: " openclaw-extension-miaoda 已在磁盘安装但未在 allow 列表中时,将其添加到 plugins.allow(实验性)",
2745
+ OldMiaodaPluginsCleanupRule = __decorate([Rule({
2746
+ key: "old_miaoda_plugins_cleanup",
2747
+ description: "当新版 openclaw-extension-miaoda 已存在时,清理过时插件引用(openclaw-feishu-greeting、openclaw-miaoda-keepalive 等)",
2707
2748
  dependsOn: ["config_syntax_check"],
2708
2749
  repairMode: "standard",
2709
- level: "critical",
2710
- profile: "standard"
2711
- })], MiaodaPluginAllowRule);
2712
- function getAllow(config) {
2713
- const plugins = config.plugins;
2714
- if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
2715
- const allow = plugins.allow;
2716
- if (!Array.isArray(allow)) return [];
2717
- return allow.filter((e) => typeof e === "string");
2718
- }
2750
+ level: "silent"
2751
+ })], OldMiaodaPluginsCleanupRule);
2719
2752
  //#endregion
2720
2753
  //#region src/rules/builtin-plugin-missing.ts
2721
2754
  /**
@@ -3473,87 +3506,54 @@ function extractScopedNameFromSpec(spec) {
3473
3506
  return at === -1 ? spec : spec.slice(0, at);
3474
3507
  }
3475
3508
  //#endregion
3476
- //#region src/rules/feishu-accounts-consistency.ts
3477
- /**
3478
- * Detects count/id mismatches in multi-agent feishu channel config.
3479
- * Single-agent configs (no `accounts`) are out of scope — handled by `feishu_channel`.
3480
- *
3481
- * Checks:
3482
- * 1. channels.feishu.accounts count == agents.list count
3483
- * 2. channels.feishu.accounts count == feishu bindings count
3484
- * 3. Main agent's bot account appId == ctx.vars.feishuAppID
3485
- * 4. agents.list id set == bindings agentId set (feishu channel)
3486
- * 5. bindings accountId set (feishu channel) == accounts key set
3487
- */
3488
- let FeishuAccountsConsistencyRule = class FeishuAccountsConsistencyRule extends DiagnoseRule {
3509
+ //#region src/rules/cleanup-install-backup-dirs.ts
3510
+ const DIR_PREFIX = ".openclaw-install-";
3511
+ function resolveExtensionsDir(configPath) {
3512
+ return node_path.default.join(node_path.default.dirname(configPath), "extensions");
3513
+ }
3514
+ function findLeftoverDirs(extensionsDir) {
3515
+ if (!fileExists(extensionsDir)) return [];
3516
+ let entries;
3517
+ try {
3518
+ entries = node_fs.default.readdirSync(extensionsDir, { withFileTypes: true });
3519
+ } catch {
3520
+ return [];
3521
+ }
3522
+ return entries.filter((e) => e.isDirectory() && e.name.startsWith(DIR_PREFIX)).map((e) => node_path.default.join(extensionsDir, e.name));
3523
+ }
3524
+ let CleanupInstallBackupDirsRule = class CleanupInstallBackupDirsRule extends DiagnoseRule {
3489
3525
  validate(ctx) {
3490
- const feishu = getNestedMap(ctx.config, "channels", "feishu");
3491
- if (!feishu) return { pass: true };
3492
- const accounts = asRecord(feishu.accounts);
3493
- if (!accounts) return { pass: true };
3494
- const accountCount = Object.keys(accounts).length;
3495
- if (accountCount === 0) return { pass: true };
3496
- const issues = [];
3497
- const agentList = getAgentList(ctx.config);
3498
- const feishuBindings = getFeishuBindings(ctx.config);
3499
- if (agentList.length > 0 && accountCount !== agentList.length) issues.push(`飞书账号数(${accountCount})与 agents.list 代理数(${agentList.length})不一致,两者应相等`);
3500
- if (feishuBindings.length > 0 && accountCount !== feishuBindings.length) issues.push(`飞书账号数(${accountCount})与 feishu 类型 bindings 数(${feishuBindings.length})不一致,两者应相等`);
3501
- const mainAgent = findMainAgent(ctx.config);
3502
- const mainId = typeof mainAgent?.id === "string" && mainAgent.id !== "" ? mainAgent.id : feishuBindings.find((b) => asRecord(b)?.agentId === "main") ? "main" : "";
3503
- if (mainId === "") issues.push("存在多账号配置(channels.feishu.accounts)但无法确定主 agent:agents.list 中没有标记 default:true 或 id:\"main\" 的代理,feishu bindings 中也没有 agentId:\"main\" 的路由");
3504
- else {
3505
- const expectedAppId = ctx.vars?.feishuAppID;
3506
- if (typeof expectedAppId === "string" && expectedAppId !== "") {
3507
- const mainBinding = feishuBindings.find((b) => asRecord(b)?.agentId === mainId);
3508
- const mainAccountId = asRecord(asRecord(mainBinding)?.match)?.accountId;
3509
- if (typeof mainAccountId === "string") {
3510
- const mainBotAcc = asRecord(accounts[mainAccountId]);
3511
- if (mainBotAcc && mainBotAcc.appId !== expectedAppId) issues.push(`主 agent(${mainId})绑定的飞书账号(${mainAccountId})的 appId 为 "${mainBotAcc.appId}",与平台下发的 feishuAppID "${expectedAppId}" 不符`);
3512
- } else if (mainBinding != null) issues.push(`主 agent(${mainId})的 feishu binding 缺少 match.accountId 字段,无法校验 appId 是否与平台下发的 feishuAppID "${expectedAppId}" 一致`);
3513
- }
3514
- }
3515
- if (agentList.length > 0 && feishuBindings.length > 0) {
3516
- const agentIds = new Set(agentList.map((a) => asRecord(a)?.id).filter((id) => typeof id === "string"));
3517
- const bindingAgentIds = new Set(feishuBindings.map((b) => asRecord(b)?.agentId).filter((id) => typeof id === "string"));
3518
- const missingInBindings = [...agentIds].filter((id) => !bindingAgentIds.has(id));
3519
- const extraInBindings = [...bindingAgentIds].filter((id) => !agentIds.has(id));
3520
- if (missingInBindings.length > 0) issues.push(`agent(${missingInBindings.join("、")})存在于 agents.list 但缺少对应的 feishu binding`);
3521
- if (extraInBindings.length > 0) issues.push(`binding 引用了 agentId(${extraInBindings.join("、")}),但该 agent 不在 agents.list 中`);
3522
- }
3523
- if (feishuBindings.length > 0) {
3524
- const accountIds = new Set(Object.keys(accounts));
3525
- const bindingAccountIds = new Set(feishuBindings.map((b) => asRecord(asRecord(b)?.match)?.accountId).filter((id) => typeof id === "string"));
3526
- const missingInBindings = [...accountIds].filter((id) => !bindingAccountIds.has(id));
3527
- const extraInBindings = [...bindingAccountIds].filter((id) => !accountIds.has(id));
3528
- if (missingInBindings.length > 0) issues.push(`飞书账号(${missingInBindings.join("、")})存在于 channels.feishu.accounts 但没有对应的 feishu binding`);
3529
- if (extraInBindings.length > 0) issues.push(`binding 引用了 accountId(${extraInBindings.join("、")}),但该账号不在 channels.feishu.accounts 中`);
3530
- }
3531
- if (issues.length === 0) return { pass: true };
3526
+ const { configPath } = ctx;
3527
+ if (!configPath) return { pass: true };
3528
+ const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
3529
+ if (dirs.length === 0) return { pass: true };
3532
3530
  return {
3533
3531
  pass: false,
3534
- message: issues.map((s, i) => `[${i + 1}] ${s}`).join(";")
3532
+ message: `extensions 目录下发现 ${dirs.length} ${DIR_PREFIX}* 脏目录需要清理`
3535
3533
  };
3536
3534
  }
3535
+ repair(ctx) {
3536
+ const { configPath } = ctx;
3537
+ if (!configPath) return;
3538
+ const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
3539
+ const failures = [];
3540
+ for (const dir of dirs) try {
3541
+ node_fs.default.rmSync(dir, {
3542
+ recursive: true,
3543
+ force: true
3544
+ });
3545
+ } catch (e) {
3546
+ failures.push(`${node_path.default.basename(dir)}: ${e.message}`);
3547
+ }
3548
+ if (dirs.length > 0 && failures.length === dirs.length) throw new Error(`cleanup_install_backup_dirs: 全部清理失败: ${failures.join("; ")}`);
3549
+ }
3537
3550
  };
3538
- FeishuAccountsConsistencyRule = __decorate([Rule({
3539
- key: "feishu_accounts_consistency",
3540
- description: "检测多 agent 配置中 channels.feishu.accounts、agents.list feishu bindings 之间的数量/ID 不一致(实验性)",
3541
- dependsOn: ["config_syntax_check"],
3542
- repairMode: "check-only",
3543
- level: "silent",
3544
- usesVars: ["feishuAppID"],
3545
- profile: "experimental"
3546
- })], FeishuAccountsConsistencyRule);
3547
- function getAgentList(config) {
3548
- const agents = getNestedMap(config, "agents");
3549
- return Array.isArray(agents?.list) ? agents.list : [];
3550
- }
3551
- function getFeishuBindings(config) {
3552
- if (!Array.isArray(config.bindings)) return [];
3553
- return config.bindings.filter((b) => {
3554
- return asRecord(asRecord(b)?.match)?.channel === "feishu";
3555
- });
3556
- }
3551
+ CleanupInstallBackupDirsRule = __decorate([Rule({
3552
+ key: "cleanup_install_backup_dirs",
3553
+ description: "清理 extensions/ 目录中升级中断遗留的 .openclaw-install-stage-* .openclaw-install-backups/ 脏目录",
3554
+ repairMode: "standard",
3555
+ level: "critical"
3556
+ })], CleanupInstallBackupDirsRule);
3557
3557
  //#endregion
3558
3558
  //#region src/check.ts
3559
3559
  /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
@@ -4325,6 +4325,7 @@ function originOf(raw) {
4325
4325
  //#region src/oss/fetchManifest.ts
4326
4326
  const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
4327
4327
  const MANIFEST_SUFFIX = ".json";
4328
+ const manifestCache = /* @__PURE__ */ new Map();
4328
4329
  async function fetchManifest(ossFileMap, tag) {
4329
4330
  const key = `${MANIFEST_PREFIX}${tag}${MANIFEST_SUFFIX}`;
4330
4331
  const url = ossFileMap[key];
@@ -4333,13 +4334,17 @@ async function fetchManifest(ossFileMap, tag) {
4333
4334
  const availStr = available.length ? available.join(", ") : "(none)";
4334
4335
  throw new Error(`manifest signed URL missing for tag "${tag}" (key ${key}). Available tags in ossFileMap: ${availStr}. Either pass an available tag or update the studio_server TCC openclaw_upgrade_config supported_versions.`);
4335
4336
  }
4337
+ const cached = manifestCache.get(url);
4338
+ if (cached) return cached;
4336
4339
  const label = `openclaw manifest tag=${tag}`;
4337
4340
  const res = await fetchWithDiag(url, { label });
4338
4341
  if (!res.ok) {
4339
4342
  const body = await readPreview(res);
4340
4343
  throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (body ? ` body=${body}` : ""));
4341
4344
  }
4342
- return await res.json();
4345
+ const manifest = await res.json();
4346
+ manifestCache.set(url, manifest);
4347
+ return manifest;
4343
4348
  }
4344
4349
  /** Best-effort body preview for HTTP-error diagnostics. OSS errors are
4345
4350
  * XML; first 256 chars usually contain the <Code> + <Message> we care
@@ -4498,6 +4503,7 @@ async function installExtension(tag, ossFileMap, opts = {}) {
4498
4503
  console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
4499
4504
  }
4500
4505
  const MEM0_PLUGIN_NAME = "openclaw-mem0-plugin";
4506
+ const PLUGINS_TO_AUTO_ENABLE = ["openclaw-lark", "openclaw-extension-miaoda"];
4501
4507
  /**
4502
4508
  * Merge each installed extension's installMetadata into openclaw.json's
4503
4509
  * plugins.installs[<pkg.name>]. Atomic write via tmp + rename.
@@ -4538,6 +4544,18 @@ function updatePluginInstalls(configPath, installedPkgs) {
4538
4544
  const entries = plugins.entries;
4539
4545
  if (!(MEM0_PLUGIN_NAME in entries)) entries[MEM0_PLUGIN_NAME] = { enabled: false };
4540
4546
  }
4547
+ for (const pkg of installedPkgs) {
4548
+ if (!PLUGINS_TO_AUTO_ENABLE.includes(pkg.name)) continue;
4549
+ if (!Array.isArray(plugins.allow)) plugins.allow = [];
4550
+ const allow = plugins.allow;
4551
+ if (!allow.includes(pkg.name)) allow.push(pkg.name);
4552
+ if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) plugins.entries = {};
4553
+ const entries = plugins.entries;
4554
+ entries[pkg.name] = {
4555
+ ...asRecord(entries[pkg.name]) ?? {},
4556
+ enabled: true
4557
+ };
4558
+ }
4541
4559
  const tmpPath = configPath + ".installs-tmp";
4542
4560
  node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
4543
4561
  moveSafe(tmpPath, configPath);
@@ -4574,1117 +4592,604 @@ function installOne$1(pkg, tarball, homeBase) {
4574
4592
  });
4575
4593
  }
4576
4594
  //#endregion
4577
- //#region src/download-resource.ts
4578
- /**
4579
- * Download + extract a config/template package to its install destination.
4580
- *
4581
- * Current manifest has all resources as format=tgz with content at the root
4582
- * (config: openclaw.json file at root; template: scripts/ dir at root), so we
4583
- * always `tar -xzf` without --strip-components into `dirname(fullInstallPath)`.
4584
- * The final artefact ends up at exactly `homeBase + pkg.installPath`.
4585
- */
4586
- async function downloadResource(tag, ossFileMap, opts) {
4587
- const homeBase = resolveHomeBase(opts.homeBase);
4588
- const pkg = (await fetchManifest(ossFileMap, tag)).packages.find((p) => p.role === opts.role && p.name === opts.name);
4589
- if (!pkg) throw new Error(`download-resource: not found in manifest: role=${opts.role} name=${opts.name}`);
4590
- const file = await downloadWithCache(pkg, ossFileMap, opts);
4591
- const fullInstallPath = node_path.default.join(homeBase, pkg.installPath);
4592
- const extractDir = opts.dir ?? node_path.default.dirname(fullInstallPath);
4593
- node_fs.default.mkdirSync(extractDir, { recursive: true });
4594
- const format = (pkg.format ?? "").toLowerCase();
4595
- const lower = pkg.ossKey.toLowerCase();
4596
- if (format === "tgz" || lower.endsWith(".tgz") || lower.endsWith(".tar.gz")) {
4597
- extractTarballTolerant(file, extractDir);
4598
- console.error(`[download-resource] ${opts.role}/${opts.name}: extracted to ${extractDir}`);
4599
- } else {
4600
- const basename = node_path.default.posix.basename(pkg.ossKey);
4601
- node_fs.default.copyFileSync(file, node_path.default.join(extractDir, basename));
4602
- console.error(`[download-resource] ${opts.role}/${opts.name}: copied ${basename} to ${extractDir}`);
4595
+ //#region src/lark-cli-init.ts
4596
+ const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
4597
+ const PE_XML_TAG = "lark-cli-pe";
4598
+ const PE_PLACEHOLDER = `
4599
+ <${PE_XML_TAG}>
4600
+ **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
4601
+ </${PE_XML_TAG}>
4602
+ `;
4603
+ function isLarkPluginInstalled(configPath) {
4604
+ const extDir = getExtensionsDir(configPath);
4605
+ return LARK_PLUGIN_NAMES.some((name) => {
4606
+ try {
4607
+ return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
4608
+ } catch {
4609
+ return false;
4610
+ }
4611
+ });
4612
+ }
4613
+ function isLarkCliAvailable() {
4614
+ try {
4615
+ return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
4616
+ encoding: "utf-8",
4617
+ timeout: 5e3,
4618
+ stdio: [
4619
+ "ignore",
4620
+ "pipe",
4621
+ "ignore"
4622
+ ]
4623
+ }).status === 0;
4624
+ } catch {
4625
+ return false;
4626
+ }
4627
+ }
4628
+ function readConfig(configPath) {
4629
+ try {
4630
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
4631
+ const parsed = loadJSON5().parse(raw);
4632
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4633
+ } catch {
4634
+ return null;
4603
4635
  }
4604
4636
  }
4605
- //#endregion
4606
- //#region src/oss/getOpenclawTag.ts
4607
4637
  /**
4608
- * Extracts the openclaw tag from the manifest key present in ossFileMap.
4609
- * Avoids passing an extra ctx field — we already know the tag from the
4610
- * well-known manifest key studio_server included.
4638
+ * Resolve the feishu app secret for the given appId.
4611
4639
  *
4612
- * Manifest key shape: builtin/manifests/openclaw/recommended/<tag>.json
4640
+ * Lookup order:
4641
+ * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
4642
+ * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
4643
+ *
4644
+ * Value interpretation:
4645
+ * - string → use directly
4646
+ * - object → secret is managed by a provider; use `feishuAppSecret` param instead
4647
+ *
4648
+ * Returns null when the secret cannot be determined.
4613
4649
  */
4614
- function getOpenclawTagFromOssFileMap(ossFileMap) {
4615
- const prefix = "builtin/manifests/openclaw/recommended/";
4616
- const suffix = ".json";
4617
- for (const key of Object.keys(ossFileMap)) if (key.startsWith(prefix) && key.endsWith(suffix)) return key.slice(39, -5);
4618
- throw new Error("cannot resolve openclaw tag: ossFileMap missing manifest key");
4619
- }
4620
- //#endregion
4621
- //#region src/reset.ts
4622
- const STEPS = [
4623
- "备份当前配置",
4624
- "生成默认配置",
4625
- "杀掉 openclaw 进程",
4626
- "等待沙箱初始化完成",
4627
- "确认 openclaw 版本",
4628
- "合并核心备份配置",
4629
- "检查启动脚本",
4630
- "安装扩展",
4631
- "启动并验证"
4632
- ];
4633
- const TOTAL_STEPS = STEPS.length;
4634
- function writeResultFile(resultFile, result) {
4635
- const dir = node_path.default.dirname(resultFile);
4636
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
4637
- const tmpPath = resultFile + ".tmp";
4638
- node_fs.default.writeFileSync(tmpPath, JSON.stringify(result), "utf-8");
4639
- moveSafe(tmpPath, resultFile);
4640
- }
4641
- function updateProgress(resultFile, step, startedAt) {
4642
- writeResultFile(resultFile, {
4643
- status: "running",
4644
- step,
4645
- totalSteps: TOTAL_STEPS,
4646
- progress: STEPS[step - 1],
4647
- startedAt
4648
- });
4649
- }
4650
- function markDone(resultFile, startedAt) {
4651
- writeResultFile(resultFile, {
4652
- status: "done",
4653
- step: TOTAL_STEPS,
4654
- totalSteps: TOTAL_STEPS,
4655
- progress: "重置完成",
4656
- startedAt,
4657
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
4658
- });
4659
- }
4660
- function markFailed(resultFile, step, error, startedAt) {
4661
- writeResultFile(resultFile, {
4662
- status: "failed",
4663
- step,
4664
- totalSteps: TOTAL_STEPS,
4665
- progress: step > 0 ? STEPS[step - 1] : "初始化",
4666
- error,
4667
- startedAt,
4668
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
4669
- });
4650
+ function resolveAppSecret(appId, config, feishuAppSecret) {
4651
+ const feishu = getNestedMap(config, "channels", "feishu");
4652
+ if (!feishu) return null;
4653
+ let rawSecret;
4654
+ if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
4655
+ else {
4656
+ const accounts = asRecord(feishu.accounts);
4657
+ if (accounts) for (const [, val] of Object.entries(accounts)) {
4658
+ const account = asRecord(val);
4659
+ if (account?.appId === appId) {
4660
+ rawSecret = account.appSecret ?? feishu.appSecret;
4661
+ break;
4662
+ }
4663
+ }
4664
+ }
4665
+ if (typeof rawSecret === "string" && rawSecret) return rawSecret;
4666
+ if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
4667
+ return null;
4670
4668
  }
4671
4669
  /**
4672
- * Download the template assets (config/openclaw.json + template/scripts) from
4673
- * OSS into a scratch directory so the existing step 2 (generateDefaultConfig)
4674
- * and step 7 (copyStartupScripts) can consume them as local files — the rest
4675
- * of the orchestrator code stays untouched.
4670
+ * Resolve the agents.md path for the given appId from the openclaw config.
4676
4671
  *
4677
- * Called once before step 1. The caller is responsible for rm -rf'ing
4678
- * stagedDir in a finally{} block after the reset completes (or fails).
4672
+ * Case 1: appId matches channels.feishu.appId (single-agent path)
4673
+ * WORKSPACE_DIR/AGENTS.md
4674
+ *
4675
+ * Case 2: appId found in channels.feishu.accounts (multi-agent path)
4676
+ * → find account key where account.appId === appId
4677
+ * → find binding where match.channel=feishu && match.accountId=that key
4678
+ * → if agentId === 'main' → WORKSPACE_DIR/agents.md
4679
+ * → else find agent in agents.list by id → agent.workspace/agents.md
4680
+ *
4681
+ * Returns null when the path cannot be determined.
4679
4682
  */
4680
- async function stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log) {
4681
- if (node_fs.default.existsSync(stagedDir)) node_fs.default.rmSync(stagedDir, {
4682
- recursive: true,
4683
- force: true
4684
- });
4685
- node_fs.default.mkdirSync(stagedDir, { recursive: true });
4686
- await downloadResource(openclawTag, ossFileMap, {
4687
- role: "config",
4688
- name: "openclaw.json",
4689
- dir: stagedDir
4690
- });
4691
- await downloadResource(openclawTag, ossFileMap, {
4692
- role: "template",
4693
- name: "scripts",
4694
- dir: configDir
4695
- });
4696
- log(`staged openclaw.json to ${stagedDir}, scripts directly to ${configDir}/scripts`);
4697
- }
4698
- /** Step 1: Backup current config as openclaw.json.bak.N */
4699
- function backupCurrentConfig(configPath, log) {
4700
- if (!fileExists(configPath)) {
4701
- log("no existing config, skip backup");
4702
- return;
4683
+ function resolveAgentsMdPath(appId, config) {
4684
+ const feishu = getNestedMap(config, "channels", "feishu");
4685
+ if (!feishu) {
4686
+ console.error("resolveAgentsMdPath: channels.feishu not found");
4687
+ return null;
4703
4688
  }
4704
- const dir = node_path.default.dirname(configPath);
4705
- let maxN = 0;
4706
- try {
4707
- for (const f of node_fs.default.readdirSync(dir)) {
4708
- const match = f.match(/^openclaw\.json\.bak\.(\d+)$/);
4709
- if (match) {
4710
- const n = parseInt(match[1], 10);
4711
- if (n > maxN) maxN = n;
4689
+ if (typeof feishu.appId === "string" && feishu.appId === appId) {
4690
+ console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
4691
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4692
+ }
4693
+ const accounts = asRecord(feishu.accounts);
4694
+ if (!accounts) {
4695
+ console.error("resolveAgentsMdPath: feishu.accounts not found");
4696
+ return null;
4697
+ }
4698
+ let accountId;
4699
+ for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
4700
+ accountId = key;
4701
+ break;
4702
+ }
4703
+ if (!accountId) {
4704
+ console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
4705
+ return null;
4706
+ }
4707
+ console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
4708
+ const bindings = Array.isArray(config.bindings) ? config.bindings : [];
4709
+ let agentId;
4710
+ for (const b of bindings) {
4711
+ const binding = asRecord(b);
4712
+ if (!binding) continue;
4713
+ const match = asRecord(binding.match);
4714
+ if (match?.channel === "feishu" && match?.accountId === accountId) {
4715
+ if (typeof binding.agentId === "string") {
4716
+ agentId = binding.agentId;
4717
+ break;
4712
4718
  }
4713
4719
  }
4714
- } catch {}
4715
- const bakPath = configPath + ".bak." + (maxN + 1);
4716
- node_fs.default.copyFileSync(configPath, bakPath);
4717
- log(`backed up to ${bakPath}`);
4718
- }
4719
- /** Step 2: Replace $$__XXX__ placeholders and write default config. */
4720
- function generateDefaultConfig(srcDir, configPath, templateVars, log) {
4721
- const srcConfigPath = node_path.default.join(srcDir, "openclaw.json");
4722
- if (!fileExists(srcConfigPath)) throw new Error("staged openclaw.json not found at " + srcConfigPath);
4723
- let content = node_fs.default.readFileSync(srcConfigPath, "utf-8");
4724
- let replaced = 0;
4725
- for (const [placeholder, value] of Object.entries(templateVars)) {
4726
- const parts = content.split(placeholder);
4727
- if (parts.length > 1) replaced += parts.length - 1;
4728
- content = parts.join(value);
4729
4720
  }
4730
- node_fs.default.writeFileSync(configPath, content, "utf-8");
4731
- log(`wrote ${configPath} (${replaced} placeholder(s) replaced, ${Object.keys(templateVars).length} provided)`);
4721
+ if (!agentId) {
4722
+ console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
4723
+ return null;
4724
+ }
4725
+ console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
4726
+ if (agentId === "main") {
4727
+ console.error("resolveAgentsMdPath: case=multi-agent-main");
4728
+ return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
4729
+ }
4730
+ const agentsObj = asRecord(config.agents);
4731
+ const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
4732
+ for (const a of list) {
4733
+ const agent = asRecord(a);
4734
+ if (agent?.id === agentId) {
4735
+ const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
4736
+ console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
4737
+ return node_path.default.join(ws, "AGENTS.md");
4738
+ }
4739
+ }
4740
+ console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
4741
+ return null;
4732
4742
  }
4733
- /** Step 3: Kill all openclaw processes. */
4734
- function killOpenclawProcesses(log) {
4735
- try {
4736
- shell("pkill -f openclaw-gateway || true", 5e3);
4737
- } catch {}
4738
- shell("sleep 2", 5e3);
4739
- log("killed openclaw-gateway processes");
4743
+ function appendPeToAgentsMd(agentsMdPath) {
4744
+ const dir = node_path.default.dirname(agentsMdPath);
4745
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
4746
+ const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
4747
+ if (existing.includes(`<${PE_XML_TAG}>`)) {
4748
+ console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
4749
+ return;
4750
+ }
4751
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
4752
+ node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
4753
+ console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
4740
4754
  }
4741
4755
  /**
4742
- * Step 4: Wait for the sandbox's own init (init_sandbox.sh / concurrent npm
4743
- * install) to finish before we start our own work. Two processes sharing
4744
- * ~/.npm cache + competing for disk/network just makes everything crawl;
4745
- * letting init finish first is the cleanest way to get exclusive access.
4746
- * Polls every 10s up to `maxWaitMs`. If the deadline is hit we fall through
4747
- * anyway — better to try than to fail the reset outright.
4748
- *
4749
- * Kept even after we switched off `npm install` because the sandbox init
4750
- * script still runs `npm install` for other packages and holds cache locks.
4756
+ * Collect every Feishu bot appId declared in the openclaw config.
4757
+ * Covers both single-agent (channels.feishu.appId) and multi-agent
4758
+ * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
4751
4759
  */
4752
- function waitForInitNpm(maxWaitMs, log) {
4753
- const deadline = Date.now() + maxWaitMs;
4754
- const ownPid = String(process.pid);
4755
- let polls = 0;
4756
- while (Date.now() < deadline) {
4757
- polls++;
4758
- let running = 0;
4759
- try {
4760
- const out = shell(`pgrep -af "init_sandbox.sh|npm install|npm i " | grep -v -- "${ownPid}" | wc -l`, 1e4);
4761
- running = parseInt(out.trim(), 10) || 0;
4762
- } catch {
4763
- log(`poll ${polls}: no concurrent npm, proceeding`);
4764
- return;
4765
- }
4766
- if (running === 0) {
4767
- log(`poll ${polls}: no concurrent npm, proceeding`);
4768
- return;
4769
- }
4770
- log(`poll ${polls}: ${running} concurrent npm/init process(es) still running, waiting 10s`);
4771
- try {
4772
- shell("sleep 10", 12e3);
4773
- } catch {}
4760
+ function collectFeishuAppIds(configPath) {
4761
+ const config = readConfig(configPath ?? CONFIG_PATH);
4762
+ if (!config) return [];
4763
+ const feishu = getNestedMap(config, "channels", "feishu");
4764
+ if (!feishu) return [];
4765
+ const appIds = /* @__PURE__ */ new Set();
4766
+ const topAppId = feishu.appId;
4767
+ if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
4768
+ const accounts = asRecord(feishu.accounts);
4769
+ if (accounts) for (const val of Object.values(accounts)) {
4770
+ const appId = asRecord(val)?.appId;
4771
+ if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
4774
4772
  }
4775
- log(`deadline (${maxWaitMs}ms) hit after ${polls} poll(s), proceeding anyway`);
4773
+ return [...appIds];
4776
4774
  }
4777
- /**
4778
- * Step 5: Install openclaw from the OSS-provided tarball at the target tag,
4779
- * then verify `openclaw --version` output contains that tag. No npm involved.
4780
- */
4781
- async function step5InstallOpenclaw(openclawTag, ossFileMap, log) {
4782
- log(`install-openclaw tag=${openclawTag}`);
4783
- await installOpenclaw(openclawTag, ossFileMap);
4784
- const out = shell("openclaw --version 2>&1 || true", 1e4).trim();
4785
- if (!out.includes(openclawTag)) throw new Error(`openclaw version verify failed: got "${out}"`);
4786
- log(`openclaw version verified: ${out}`);
4787
- }
4788
- /** Step 6: Merge coreBackup from resetData + ensure allowedOrigins. */
4789
- function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
4790
- const JSON5 = loadJSON5();
4791
- const backup = resetData.coreBackup;
4792
- if (backup) {
4793
- const config = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
4794
- const merged = [];
4795
- if (backup.agents && backup.agents.length > 0) {
4796
- if (!config.agents) config.agents = {};
4797
- const agents = config.agents;
4798
- if (!Array.isArray(agents.list)) agents.list = [];
4799
- const configDir = node_path.default.dirname(configPath);
4800
- for (const agent of backup.agents) {
4801
- const enriched = {
4802
- id: agent.id,
4803
- name: agent.id,
4804
- workspace: agent.workspace,
4805
- agentDir: configDir + "/agents/" + agent.id + "/agent"
4806
- };
4807
- agents.list.push(enriched);
4808
- }
4809
- merged.push(`agents(+${backup.agents.length})`);
4810
- const list = agents.list;
4811
- let mainIdx = list.findIndex((a) => a.id === "main");
4812
- if (mainIdx < 0) {
4813
- list.unshift({ id: "main" });
4814
- mainIdx = 0;
4815
- }
4816
- list[mainIdx].subagents = { allowAgents: ["*"] };
4817
- list[mainIdx].default = true;
4818
- merged.push("main-team-mode");
4819
- const feishu = config.channels?.feishu;
4820
- if (feishu) {
4821
- if (!feishu.accounts) feishu.accounts = {};
4822
- const accounts = feishu.accounts;
4823
- const defaultAccount = {};
4824
- for (const key of [
4825
- "dmPolicy",
4826
- "allowFrom",
4827
- "groupPolicy",
4828
- "groupAllowFrom"
4829
- ]) if (feishu[key] !== void 0) defaultAccount[key] = feishu[key];
4830
- if (Object.keys(defaultAccount).length > 0) {
4831
- accounts.default = defaultAccount;
4832
- merged.push("accounts.default");
4833
- }
4834
- }
4835
- }
4836
- if (backup.bindings && backup.bindings.length > 0) {
4837
- config.bindings = backup.bindings;
4838
- merged.push("bindings");
4839
- }
4840
- const backupAccounts = backup.channels?.feishu?.accounts;
4841
- if (backupAccounts && Object.keys(backupAccounts).length > 0) {
4842
- if (!config.channels) config.channels = {};
4843
- const ch = config.channels;
4844
- if (!ch.feishu) ch.feishu = {};
4845
- const feishu = ch.feishu;
4846
- if (!feishu.accounts) feishu.accounts = {};
4847
- Object.assign(feishu.accounts, backupAccounts);
4848
- merged.push("channels.feishu.accounts");
4849
- }
4850
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
4851
- log(`merged from coreBackup: [${merged.join(", ") || "nothing"}]`);
4852
- } else log("no coreBackup in resetData, skip multi-agent merge");
4853
- const expectedOrigins = Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
4854
- if (expectedOrigins.length === 0) {
4855
- log("no expectedOrigins provided");
4856
- return;
4857
- }
4858
- const config = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
4859
- if (!config.gateway) config.gateway = {};
4860
- const gw = config.gateway;
4861
- if (!gw.controlUi) gw.controlUi = {};
4862
- const cui = gw.controlUi;
4863
- const current = Array.isArray(cui.allowedOrigins) ? cui.allowedOrigins.filter((o) => typeof o === "string") : [];
4864
- if (current.includes("*")) {
4865
- log("allowedOrigins already contains \"*\", skip origin merge");
4866
- return;
4867
- }
4868
- const seen = new Set(current);
4869
- const added = [];
4870
- const mergedOrigins = [...current];
4871
- for (const o of expectedOrigins) if (!seen.has(o)) {
4872
- mergedOrigins.push(o);
4873
- seen.add(o);
4874
- added.push(o);
4875
- }
4876
- cui.allowedOrigins = mergedOrigins;
4877
- node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
4878
- log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
4879
- }
4880
- /**
4881
- * Step 7: Verify startup scripts landed in configDir/scripts/.
4882
- *
4883
- * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
4884
- * there's no intermediate copy any more. This step is now a verification gate
4885
- * (rather than a copy action) so the step count stays at 9 and we fail early
4886
- * if the template tgz didn't carry a scripts/ dir.
4887
- */
4888
- function verifyStartupScripts(configDir, log) {
4889
- const targetScriptsDir = node_path.default.join(configDir, "scripts");
4890
- if (!node_fs.default.existsSync(targetScriptsDir)) throw new Error(`scripts dir missing at ${targetScriptsDir} — template download failed?`);
4891
- log(`scripts dir present at ${targetScriptsDir}`);
4892
- }
4893
- /**
4894
- * Step 8: Install all extensions listed in the OSS manifest at `openclawTag`.
4895
- * Replaces the old `plugins update --all` / pre-packed tar.gz flow — the
4896
- * manifest is now the single source of truth for which extensions ship.
4897
- */
4898
- async function step8InstallExtensions(openclawTag, ossFileMap, log) {
4899
- log(`install-extension --all tag=${openclawTag}`);
4900
- await installExtension(openclawTag, ossFileMap, {
4901
- all: true,
4902
- skipConfigUpdate: true
4903
- });
4904
- log("extensions installed");
4905
- }
4906
- /** Step 9: Write secrets/provider key files and restart openclaw. */
4907
- function writeSecretsAndRestart(vars, resetData, configDir, log) {
4908
- if (resetData.secretsContent && vars.secretsFilePath) {
4909
- writeFile(vars.secretsFilePath, resetData.secretsContent);
4910
- log(`wrote secrets to ${vars.secretsFilePath}`);
4911
- }
4912
- if (resetData.providerKeyContent && vars.providerFilePath) {
4913
- writeFile(vars.providerFilePath, resetData.providerKeyContent);
4914
- log(`wrote provider key to ${vars.providerFilePath}`);
4915
- }
4916
- const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
4917
- if (fileExists(restartScript)) {
4918
- const t = Date.now();
4919
- shell(`bash '${restartScript}'`, 3e4);
4920
- log(`restart.sh done in ${Date.now() - t}ms`);
4921
- } else log(`no restart.sh at ${restartScript}, skip`);
4922
- }
4923
- async function runReset(input, taskId, resultFile) {
4924
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4925
- const { configPath, vars, resetData } = input;
4926
- const configDir = node_path.default.dirname(configPath);
4927
- const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
4928
- let currentStep = 0;
4929
- let stepStartedAt = Date.now();
4930
- const stepResults = [];
4931
- const log = makeLogger(resetLogFile(taskId));
4932
- log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
4933
- log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
4934
- const ossFileMap = resetData.ossFileMap;
4935
- if (!ossFileMap || Object.keys(ossFileMap).length === 0) {
4936
- const err = "resetData.ossFileMap missing or empty";
4937
- log(`ERROR: ${err}`);
4938
- markFailed(resultFile, 0, err, startedAt);
4775
+ function runLarkCliInit(opts) {
4776
+ const configPath = opts.configPath ?? CONFIG_PATH;
4777
+ if (!isLarkPluginInstalled(configPath)) {
4778
+ console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
4939
4779
  return {
4940
- success: false,
4941
- steps: stepResults,
4942
- error: err,
4943
- failedStep: 0
4780
+ ok: true,
4781
+ skipped: true,
4782
+ skipReason: "openclaw-lark plugin not installed"
4944
4783
  };
4945
4784
  }
4946
- let openclawTag;
4947
- if (resetData.openclawTag) openclawTag = resetData.openclawTag;
4948
- else try {
4949
- openclawTag = getOpenclawTagFromOssFileMap(ossFileMap);
4950
- } catch (e) {
4951
- const err = e.message;
4952
- log(`ERROR: ${err}`);
4953
- markFailed(resultFile, 0, err, startedAt);
4785
+ if (!isLarkCliAvailable()) {
4786
+ console.error("lark-cli-init: skipping lark-cli command not found");
4954
4787
  return {
4955
- success: false,
4956
- steps: stepResults,
4957
- error: err,
4958
- failedStep: 0
4788
+ ok: true,
4789
+ skipped: true,
4790
+ skipReason: "lark-cli command not found"
4959
4791
  };
4960
4792
  }
4961
- log(`openclawTag=${openclawTag}`);
4962
- process.on("uncaughtException", (err) => {
4963
- log(`FATAL uncaughtException: ${err.message}\n${err.stack ?? ""}`);
4964
- markFailed(resultFile, currentStep, `uncaught exception: ${err.message}`, startedAt);
4965
- process.exit(1);
4966
- });
4967
- process.on("unhandledRejection", (reason) => {
4968
- log(`FATAL unhandledRejection: ${String(reason)}`);
4969
- markFailed(resultFile, currentStep, `unhandled rejection: ${reason}`, startedAt);
4970
- process.exit(1);
4793
+ const config = readConfig(configPath);
4794
+ if (!config) return {
4795
+ ok: false,
4796
+ error: `could not read config at ${configPath}`
4797
+ };
4798
+ const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
4799
+ console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
4800
+ if (!agentsMdPath) return {
4801
+ ok: false,
4802
+ error: `could not resolve agents.md path for appId=${opts.appId}`
4803
+ };
4804
+ const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
4805
+ if (!appSecret) return {
4806
+ ok: false,
4807
+ error: `could not resolve appSecret for appId=${opts.appId}`
4808
+ };
4809
+ console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
4810
+ const initRes = (0, node_child_process.spawnSync)("lark-cli", [
4811
+ "config",
4812
+ "init",
4813
+ "--name",
4814
+ opts.appId,
4815
+ "--app-id",
4816
+ opts.appId,
4817
+ "--brand",
4818
+ "feishu",
4819
+ "--app-secret-stdin",
4820
+ "--force-init"
4821
+ ], {
4822
+ stdio: [
4823
+ "pipe",
4824
+ "pipe",
4825
+ "pipe"
4826
+ ],
4827
+ encoding: "utf-8",
4828
+ input: appSecret
4971
4829
  });
4972
- /** Advance to the next step, recording the previous step's success
4973
- * duration and updating the on-disk progress file. */
4974
- const step = (n) => {
4975
- if (currentStep > 0) {
4976
- const dur = Date.now() - stepStartedAt;
4977
- log(`step ${currentStep} "${STEPS[currentStep - 1]}" done in ${dur}ms`);
4978
- stepResults.push({
4979
- step: currentStep,
4980
- name: STEPS[currentStep - 1],
4981
- status: "ok",
4982
- durationMs: dur
4983
- });
4984
- }
4985
- currentStep = n;
4986
- stepStartedAt = Date.now();
4987
- log(`--- step ${n}/${TOTAL_STEPS}: ${STEPS[n - 1]} ---`);
4988
- updateProgress(resultFile, n, startedAt);
4830
+ const configInitStdout = initRes.stdout?.trim() || void 0;
4831
+ const configInitStderr = initRes.stderr?.trim() || void 0;
4832
+ if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
4833
+ if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
4834
+ if (initRes.error) return {
4835
+ ok: false,
4836
+ configInitStdout,
4837
+ configInitStderr,
4838
+ error: `lark-cli config init spawn error: ${initRes.error.message}`
4989
4839
  };
4990
- try {
4991
- await stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log);
4992
- step(1);
4993
- backupCurrentConfig(configPath, log);
4994
- step(2);
4995
- generateDefaultConfig(stagedDir, configPath, resetData.templateVars, log);
4996
- step(3);
4997
- killOpenclawProcesses(log);
4998
- step(4);
4999
- waitForInitNpm(10 * 6e4, log);
5000
- step(5);
5001
- await step5InstallOpenclaw(openclawTag, ossFileMap, log);
5002
- step(6);
5003
- mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
5004
- step(7);
5005
- verifyStartupScripts(configDir, log);
5006
- step(8);
5007
- await step8InstallExtensions(openclawTag, ossFileMap, log);
5008
- step(9);
5009
- writeSecretsAndRestart(vars, resetData, configDir, log);
5010
- const lastDur = Date.now() - stepStartedAt;
5011
- log(`step 9 "${STEPS[8]}" done in ${lastDur}ms`);
5012
- stepResults.push({
5013
- step: 9,
5014
- name: STEPS[8],
5015
- status: "ok",
5016
- durationMs: lastDur
5017
- });
5018
- log("=== reset completed successfully ===");
5019
- markDone(resultFile, startedAt);
5020
- return {
5021
- success: true,
5022
- steps: stepResults
5023
- };
5024
- } catch (e) {
5025
- const err = e.message;
5026
- const dur = Date.now() - stepStartedAt;
5027
- log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${dur}ms: ${err}\n${e.stack ?? ""}`);
5028
- stepResults.push({
5029
- step: currentStep,
5030
- name: STEPS[currentStep - 1] ?? "init",
5031
- status: "fail",
5032
- durationMs: dur,
5033
- error: err
4840
+ if (initRes.status !== 0) return {
4841
+ ok: false,
4842
+ configInitExitCode: initRes.status ?? void 0,
4843
+ configInitStdout,
4844
+ configInitStderr,
4845
+ error: `lark-cli config init exited with code ${initRes.status}`
4846
+ };
4847
+ appendPeToAgentsMd(agentsMdPath);
4848
+ return {
4849
+ ok: true,
4850
+ configInitExitCode: 0,
4851
+ agentsMdPath
4852
+ };
4853
+ }
4854
+ //#endregion
4855
+ //#region ../../openclaw-slardar/lib/client.js
4856
+ var import_index_cjs = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports) => {
4857
+ Object.defineProperty(exports, "__esModule", { value: true });
4858
+ var DEFAULT_SIZE = 10;
4859
+ var DEFAULT_WAIT = 1e3;
4860
+ var stringifyBatch = function(list) {
4861
+ return JSON.stringify({
4862
+ ev_type: "batch",
4863
+ list
5034
4864
  });
5035
- markFailed(resultFile, currentStep, err, startedAt);
5036
- return {
5037
- success: false,
5038
- steps: stepResults,
5039
- error: err,
5040
- failedStep: currentStep
4865
+ };
4866
+ function createBatchSender(config) {
4867
+ var transport = config.transport;
4868
+ var endpoint = config.endpoint, _a = config.size, size = _a === void 0 ? DEFAULT_SIZE : _a, _b = config.wait, wait = _b === void 0 ? DEFAULT_WAIT : _b;
4869
+ var batch = [];
4870
+ var tid = 0;
4871
+ var fail;
4872
+ var success;
4873
+ var sender = {
4874
+ getSize: function() {
4875
+ return size;
4876
+ },
4877
+ getWait: function() {
4878
+ return wait;
4879
+ },
4880
+ setSize: function(v) {
4881
+ size = v;
4882
+ },
4883
+ setWait: function(v) {
4884
+ wait = v;
4885
+ },
4886
+ getEndpoint: function() {
4887
+ return endpoint;
4888
+ },
4889
+ setEndpoint: function(v) {
4890
+ endpoint = v;
4891
+ },
4892
+ send: function(e) {
4893
+ batch.push(e);
4894
+ if (batch.length >= size) sendBatch.call(this);
4895
+ clearTimeout(tid);
4896
+ tid = setTimeout(sendBatch.bind(this), wait);
4897
+ },
4898
+ flush: function() {
4899
+ clearTimeout(tid);
4900
+ sendBatch.call(this);
4901
+ },
4902
+ getBatchData: function() {
4903
+ return batch.length ? stringifyBatch(batch) : "";
4904
+ },
4905
+ clear: function() {
4906
+ clearTimeout(tid);
4907
+ batch = [];
4908
+ },
4909
+ fail: function(cb) {
4910
+ fail = cb;
4911
+ },
4912
+ success: function(cb) {
4913
+ success = cb;
4914
+ }
5041
4915
  };
5042
- } finally {
5043
- try {
5044
- node_fs.default.rmSync(stagedDir, {
5045
- recursive: true,
5046
- force: true
4916
+ function sendBatch() {
4917
+ if (!batch.length) return;
4918
+ var data = this.getBatchData();
4919
+ transport.post({
4920
+ url: endpoint,
4921
+ data,
4922
+ fail: function(err) {
4923
+ fail && fail(err, data);
4924
+ },
4925
+ success: function() {
4926
+ success && success(data);
4927
+ }
5047
4928
  });
5048
- } catch {}
5049
- }
5050
- }
5051
- //#endregion
5052
- //#region src/get-reset-task.ts
5053
- /**
5054
- * Long-poll for a reset task result.
5055
- * Reads the result file every 1s for up to 30s.
5056
- * Returns immediately on terminal states (done/failed).
5057
- */
5058
- function getResetTask(taskId) {
5059
- const resultFile = resetResultFile(taskId);
5060
- const deadline = Date.now() + 3e4;
5061
- while (Date.now() < deadline) {
5062
- if (!node_fs.default.existsSync(resultFile)) {
5063
- sleepSync(1e3);
5064
- continue;
4929
+ batch = [];
5065
4930
  }
5066
- try {
5067
- const content = node_fs.default.readFileSync(resultFile, "utf-8");
5068
- const result = JSON.parse(content);
5069
- if (result.status === "done" || result.status === "failed") return result;
5070
- } catch {}
5071
- sleepSync(1e3);
4931
+ return sender;
5072
4932
  }
5073
- if (node_fs.default.existsSync(resultFile)) try {
5074
- return JSON.parse(node_fs.default.readFileSync(resultFile, "utf-8"));
5075
- } catch {}
5076
- return {
5077
- status: "running",
5078
- progress: "等待中..."
4933
+ /*! *****************************************************************************
4934
+ Copyright (c) Microsoft Corporation.
4935
+
4936
+ Permission to use, copy, modify, and/or distribute this software for any
4937
+ purpose with or without fee is hereby granted.
4938
+
4939
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
4940
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
4941
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
4942
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
4943
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
4944
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
4945
+ PERFORMANCE OF THIS SOFTWARE.
4946
+ ***************************************************************************** */
4947
+ var __assign = function() {
4948
+ __assign = Object.assign || function __assign(t) {
4949
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4950
+ s = arguments[i];
4951
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
4952
+ }
4953
+ return t;
4954
+ };
4955
+ return __assign.apply(this, arguments);
5079
4956
  };
5080
- }
5081
- /**
5082
- * Synchronous sleep using Atomics.wait on a shared buffer.
5083
- */
5084
- function sleepSync(ms) {
5085
- const buf = new SharedArrayBuffer(4);
5086
- const arr = new Int32Array(buf);
5087
- Atomics.wait(arr, 0, 0, ms);
5088
- }
5089
- //#endregion
5090
- //#region src/lark-cli-init.ts
5091
- const LARK_PLUGIN_NAMES = ["openclaw-lark", "feishu-openclaw-plugin"];
5092
- const PE_XML_TAG = "lark-cli-pe";
5093
- const PE_PLACEHOLDER = `
5094
- <${PE_XML_TAG}>
5095
- **【强制要求 - 无例外】飞书所有操作必须先读 feishu-lark-cli skill。未读skill不得调用任何飞书工具,违者视为操作失误。**
5096
- </${PE_XML_TAG}>
5097
- `;
5098
- function isLarkPluginInstalled(configPath) {
5099
- const extDir = getExtensionsDir(configPath);
5100
- return LARK_PLUGIN_NAMES.some((name) => {
4957
+ function __values(o) {
4958
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
4959
+ if (m) return m.call(o);
4960
+ if (o && typeof o.length === "number") return { next: function() {
4961
+ if (o && i >= o.length) o = void 0;
4962
+ return {
4963
+ value: o && o[i++],
4964
+ done: !o
4965
+ };
4966
+ } };
4967
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
4968
+ }
4969
+ function __read(o, n) {
4970
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
4971
+ if (!m) return o;
4972
+ var i = m.call(o), r, ar = [], e;
5101
4973
  try {
5102
- return node_fs.default.existsSync(node_path.default.join(extDir, name, "package.json"));
5103
- } catch {
5104
- return false;
4974
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
4975
+ } catch (error) {
4976
+ e = { error };
4977
+ } finally {
4978
+ try {
4979
+ if (r && !r.done && (m = i["return"])) m.call(i);
4980
+ } finally {
4981
+ if (e) throw e.error;
4982
+ }
5105
4983
  }
5106
- });
5107
- }
5108
- function isLarkCliAvailable() {
5109
- try {
5110
- return (0, node_child_process.spawnSync)("lark-cli", ["--version"], {
5111
- encoding: "utf-8",
5112
- timeout: 5e3,
5113
- stdio: [
5114
- "ignore",
5115
- "pipe",
5116
- "ignore"
5117
- ]
5118
- }).status === 0;
5119
- } catch {
5120
- return false;
4984
+ return ar;
5121
4985
  }
5122
- }
5123
- function readConfig(configPath) {
5124
- try {
5125
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
5126
- const parsed = loadJSON5().parse(raw);
5127
- return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5128
- } catch {
5129
- return null;
4986
+ function __spreadArray(to, from, pack) {
4987
+ if (pack || arguments.length === 2) {
4988
+ for (var i = 0, l = from.length, ar; i < l; i++) if (ar || !(i in from)) {
4989
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
4990
+ ar[i] = from[i];
4991
+ }
4992
+ }
4993
+ return to.concat(ar || Array.prototype.slice.call(from));
5130
4994
  }
5131
- }
5132
- /**
5133
- * Resolve the feishu app secret for the given appId.
5134
- *
5135
- * Lookup order:
5136
- * 1. channels.feishu.appSecret (single-agent: feishu.appId === appId)
5137
- * 2. channels.feishu.accounts[key].appSecret (multi-agent: account.appId === appId)
5138
- *
5139
- * Value interpretation:
5140
- * - string → use directly
5141
- * - object → secret is managed by a provider; use `feishuAppSecret` param instead
5142
- *
5143
- * Returns null when the secret cannot be determined.
5144
- */
5145
- function resolveAppSecret(appId, config, feishuAppSecret) {
5146
- const feishu = getNestedMap(config, "channels", "feishu");
5147
- if (!feishu) return null;
5148
- let rawSecret;
5149
- if (typeof feishu.appId === "string" && feishu.appId === appId) rawSecret = feishu.appSecret;
5150
- else {
5151
- const accounts = asRecord(feishu.accounts);
5152
- if (accounts) for (const [, val] of Object.entries(accounts)) {
5153
- const account = asRecord(val);
5154
- if (account?.appId === appId) {
5155
- rawSecret = account.appSecret ?? feishu.appSecret;
5156
- break;
5157
- }
5158
- }
4995
+ var noop = function() {
4996
+ return {};
4997
+ };
4998
+ function id(v) {
4999
+ return v;
5159
5000
  }
5160
- if (typeof rawSecret === "string" && rawSecret) return rawSecret;
5161
- if (rawSecret != null && typeof rawSecret === "object") return feishuAppSecret ?? null;
5162
- return null;
5163
- }
5164
- /**
5165
- * Resolve the agents.md path for the given appId from the openclaw config.
5166
- *
5167
- * Case 1: appId matches channels.feishu.appId (single-agent path)
5168
- * → WORKSPACE_DIR/AGENTS.md
5169
- *
5170
- * Case 2: appId found in channels.feishu.accounts (multi-agent path)
5171
- * → find account key where account.appId === appId
5172
- * → find binding where match.channel=feishu && match.accountId=that key
5173
- * → if agentId === 'main' → WORKSPACE_DIR/agents.md
5174
- * → else find agent in agents.list by id → agent.workspace/agents.md
5175
- *
5176
- * Returns null when the path cannot be determined.
5177
- */
5178
- function resolveAgentsMdPath(appId, config) {
5179
- const feishu = getNestedMap(config, "channels", "feishu");
5180
- if (!feishu) {
5181
- console.error("resolveAgentsMdPath: channels.feishu not found");
5182
- return null;
5001
+ function isObject(o) {
5002
+ return typeof o === "object" && o !== null;
5183
5003
  }
5184
- if (typeof feishu.appId === "string" && feishu.appId === appId) {
5185
- console.error(`resolveAgentsMdPath: case=single-agent feishu.appId=${feishu.appId}`);
5186
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
5004
+ var objProto = Object.prototype;
5005
+ function isArray(o) {
5006
+ return objProto.toString.call(o) === "[object Array]";
5187
5007
  }
5188
- const accounts = asRecord(feishu.accounts);
5189
- if (!accounts) {
5190
- console.error("resolveAgentsMdPath: feishu.accounts not found");
5191
- return null;
5008
+ function isBoolean(o) {
5009
+ return typeof o === "boolean";
5192
5010
  }
5193
- let accountId;
5194
- for (const [key, val] of Object.entries(accounts)) if (asRecord(val)?.appId === appId) {
5195
- accountId = key;
5196
- break;
5011
+ function isNumber(o) {
5012
+ return typeof o === "number";
5197
5013
  }
5198
- if (!accountId) {
5199
- console.error(`resolveAgentsMdPath: no account found with appId=${appId} in feishu.accounts keys=[${Object.keys(accounts).join(",")}]`);
5200
- return null;
5014
+ function isString(o) {
5015
+ return typeof o === "string";
5201
5016
  }
5202
- console.error(`resolveAgentsMdPath: found accountId=${accountId}`);
5203
- const bindings = Array.isArray(config.bindings) ? config.bindings : [];
5204
- let agentId;
5205
- for (const b of bindings) {
5206
- const binding = asRecord(b);
5207
- if (!binding) continue;
5208
- const match = asRecord(binding.match);
5209
- if (match?.channel === "feishu" && match?.accountId === accountId) {
5210
- if (typeof binding.agentId === "string") {
5211
- agentId = binding.agentId;
5212
- break;
5213
- }
5017
+ function arrayIncludes(array, value) {
5018
+ if (!isArray(array)) return false;
5019
+ if (array.length === 0) return false;
5020
+ var k = 0;
5021
+ while (k < array.length) {
5022
+ if (array[k] === value) return true;
5023
+ k++;
5214
5024
  }
5025
+ return false;
5215
5026
  }
5216
- if (!agentId) {
5217
- console.error(`resolveAgentsMdPath: no binding found for accountId=${accountId} in bindings(count=${bindings.length})`);
5218
- return null;
5219
- }
5220
- console.error(`resolveAgentsMdPath: found agentId=${agentId}`);
5221
- if (agentId === "main") {
5222
- console.error("resolveAgentsMdPath: case=multi-agent-main");
5223
- return node_path.default.join(WORKSPACE_DIR, "workspace", "AGENTS.md");
5224
- }
5225
- const agentsObj = asRecord(config.agents);
5226
- const list = Array.isArray(agentsObj?.list) ? agentsObj.list : [];
5227
- for (const a of list) {
5228
- const agent = asRecord(a);
5229
- if (agent?.id === agentId) {
5230
- const ws = typeof agent.workspace === "string" ? agent.workspace : node_path.default.join(WORKSPACE_DIR, "workspace");
5231
- console.error(`resolveAgentsMdPath: case=multi-agent-custom agentId=${agentId} workspace=${ws}`);
5232
- return node_path.default.join(ws, "AGENTS.md");
5027
+ var arrayRemove = function(arr, e) {
5028
+ if (!isArray(arr)) return arr;
5029
+ var i = arr.indexOf(e);
5030
+ if (i >= 0) {
5031
+ var arr_ = arr.slice();
5032
+ arr_.splice(i, 1);
5033
+ return arr_;
5034
+ }
5035
+ return arr;
5036
+ };
5037
+ /**
5038
+ * 按路径访问对象属性
5039
+ * @param target 待访问对象
5040
+ * @param property 访问属性路径
5041
+ * @param { (target: any, property: string): any } visitor 访问器
5042
+ */
5043
+ var safeVisit = function(target, path, visitor) {
5044
+ var _a, _b;
5045
+ var _c = __read(path.split(".")), method = _c[0], rest = _c.slice(1);
5046
+ while (target && rest.length > 0) {
5047
+ target = target[method];
5048
+ _a = rest, _b = __read(_a), method = _b[0], rest = _b.slice(1);
5049
+ }
5050
+ if (!target) return;
5051
+ return visitor(target, method);
5052
+ };
5053
+ function safeStringify(a) {
5054
+ try {
5055
+ return isString(a) ? a : JSON.stringify(a);
5056
+ } catch (err) {
5057
+ return "[FAILED_TO_STRINGIFY]:" + String(err);
5233
5058
  }
5234
5059
  }
5235
- console.error(`resolveAgentsMdPath: agentId=${agentId} not found in agents.list(count=${list.length})`);
5236
- return null;
5237
- }
5238
- function appendPeToAgentsMd(agentsMdPath) {
5239
- const dir = node_path.default.dirname(agentsMdPath);
5240
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
5241
- const existing = node_fs.default.existsSync(agentsMdPath) ? node_fs.default.readFileSync(agentsMdPath, "utf-8") : "";
5242
- if (existing.includes(`<${PE_XML_TAG}>`)) {
5243
- console.error(`lark-cli-init: <${PE_XML_TAG}> already present in ${agentsMdPath}, skipping`);
5244
- return;
5245
- }
5246
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
5247
- node_fs.default.appendFileSync(agentsMdPath, `${sep}${PE_PLACEHOLDER}`, "utf-8");
5248
- console.error(`lark-cli-init: appended PE placeholder to ${agentsMdPath}`);
5249
- }
5250
- /**
5251
- * Collect every Feishu bot appId declared in the openclaw config.
5252
- * Covers both single-agent (channels.feishu.appId) and multi-agent
5253
- * (channels.feishu.accounts[*].appId) layouts. Returns a deduplicated list.
5254
- */
5255
- function collectFeishuAppIds(configPath) {
5256
- const config = readConfig(configPath ?? CONFIG_PATH);
5257
- if (!config) return [];
5258
- const feishu = getNestedMap(config, "channels", "feishu");
5259
- if (!feishu) return [];
5260
- const appIds = /* @__PURE__ */ new Set();
5261
- const topAppId = feishu.appId;
5262
- if (typeof topAppId === "string" && topAppId.trim()) appIds.add(topAppId.trim());
5263
- const accounts = asRecord(feishu.accounts);
5264
- if (accounts) for (const val of Object.values(accounts)) {
5265
- const appId = asRecord(val)?.appId;
5266
- if (typeof appId === "string" && appId.trim()) appIds.add(appId.trim());
5267
- }
5268
- return [...appIds];
5269
- }
5270
- function runLarkCliInit(opts) {
5271
- const configPath = opts.configPath ?? CONFIG_PATH;
5272
- if (!isLarkPluginInstalled(configPath)) {
5273
- console.error("lark-cli-init: skipping — openclaw-lark plugin not installed");
5274
- return {
5275
- ok: true,
5276
- skipped: true,
5277
- skipReason: "openclaw-lark plugin not installed"
5060
+ function createContextAgent() {
5061
+ var context = {};
5062
+ var stringified = {};
5063
+ var contextAgent = {
5064
+ set: function(k, v) {
5065
+ context[k] = v;
5066
+ stringified[k] = safeStringify(v);
5067
+ return contextAgent;
5068
+ },
5069
+ merge: function(ctx) {
5070
+ context = __assign(__assign({}, context), ctx);
5071
+ Object.keys(ctx).forEach(function(key) {
5072
+ stringified[key] = safeStringify(ctx[key]);
5073
+ });
5074
+ return contextAgent;
5075
+ },
5076
+ delete: function(k) {
5077
+ delete context[k];
5078
+ delete stringified[k];
5079
+ return contextAgent;
5080
+ },
5081
+ clear: function() {
5082
+ context = {};
5083
+ stringified = {};
5084
+ return contextAgent;
5085
+ },
5086
+ get: function(k) {
5087
+ return stringified[k];
5088
+ },
5089
+ toString: function() {
5090
+ return __assign({}, stringified);
5091
+ }
5278
5092
  };
5093
+ return contextAgent;
5279
5094
  }
5280
- if (!isLarkCliAvailable()) {
5281
- console.error("lark-cli-init: skipping lark-cli command not found");
5282
- return {
5283
- ok: true,
5284
- skipped: true,
5285
- skipReason: "lark-cli command not found"
5095
+ var getPrintString = function() {
5096
+ if ("".padStart) return function(str, prefixLength) {
5097
+ if (prefixLength === void 0) prefixLength = 8;
5098
+ return str.padStart(prefixLength, " ");
5099
+ };
5100
+ return function(str) {
5101
+ return str;
5286
5102
  };
5287
- }
5288
- const config = readConfig(configPath);
5289
- if (!config) return {
5290
- ok: false,
5291
- error: `could not read config at ${configPath}`
5292
5103
  };
5293
- const agentsMdPath = resolveAgentsMdPath(opts.appId, config);
5294
- console.error(`lark-cli-init: resolved agents.md path=${agentsMdPath ?? "(null)"}`);
5295
- if (!agentsMdPath) return {
5296
- ok: false,
5297
- error: `could not resolve agents.md path for appId=${opts.appId}`
5104
+ var printString = getPrintString();
5105
+ var errCount = 0;
5106
+ var error = function() {
5107
+ var args = [];
5108
+ for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
5109
+ console.error.apply(console, __spreadArray([
5110
+ "[SDK]",
5111
+ Date.now(),
5112
+ printString("" + errCount++)
5113
+ ], __read(args), false));
5298
5114
  };
5299
- const appSecret = resolveAppSecret(opts.appId, config, opts.feishuAppSecret);
5300
- if (!appSecret) return {
5301
- ok: false,
5302
- error: `could not resolve appSecret for appId=${opts.appId}`
5115
+ var warnCount = 0;
5116
+ var warn = function() {
5117
+ var args = [];
5118
+ for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
5119
+ console.warn.apply(console, __spreadArray([
5120
+ "[SDK]",
5121
+ Date.now(),
5122
+ printString("" + warnCount++)
5123
+ ], __read(args), false));
5303
5124
  };
5304
- console.error(`lark-cli-init: running lark-cli config init --name ${opts.appId} --app-id ${opts.appId} --brand feishu --app-secret-stdin --force-init`);
5305
- const initRes = (0, node_child_process.spawnSync)("lark-cli", [
5306
- "config",
5307
- "init",
5308
- "--name",
5309
- opts.appId,
5310
- "--app-id",
5311
- opts.appId,
5312
- "--brand",
5313
- "feishu",
5314
- "--app-secret-stdin",
5315
- "--force-init"
5316
- ], {
5317
- stdio: [
5318
- "pipe",
5319
- "pipe",
5320
- "pipe"
5321
- ],
5322
- encoding: "utf-8",
5323
- input: appSecret
5324
- });
5325
- const configInitStdout = initRes.stdout?.trim() || void 0;
5326
- const configInitStderr = initRes.stderr?.trim() || void 0;
5327
- if (configInitStdout) console.error(`lark-cli config init stdout: ${configInitStdout}`);
5328
- if (configInitStderr) console.error(`lark-cli config init stderr: ${configInitStderr}`);
5329
- if (initRes.error) return {
5330
- ok: false,
5331
- configInitStdout,
5332
- configInitStderr,
5333
- error: `lark-cli config init spawn error: ${initRes.error.message}`
5334
- };
5335
- if (initRes.status !== 0) return {
5336
- ok: false,
5337
- configInitExitCode: initRes.status ?? void 0,
5338
- configInitStdout,
5339
- configInitStderr,
5340
- error: `lark-cli config init exited with code ${initRes.status}`
5341
- };
5342
- appendPeToAgentsMd(agentsMdPath);
5343
- return {
5344
- ok: true,
5345
- configInitExitCode: 0,
5346
- agentsMdPath
5125
+ var isHitBySampleRate = function(sampleRate) {
5126
+ if (Math.random() < Number(sampleRate)) return true;
5127
+ return false;
5347
5128
  };
5348
- }
5349
- //#endregion
5350
- //#region ../../openclaw-slardar/lib/client.js
5351
- var import_index_cjs = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports) => {
5352
- Object.defineProperty(exports, "__esModule", { value: true });
5353
- var DEFAULT_SIZE = 10;
5354
- var DEFAULT_WAIT = 1e3;
5355
- var stringifyBatch = function(list) {
5356
- return JSON.stringify({
5357
- ev_type: "batch",
5358
- list
5359
- });
5129
+ var isHitByRandom = function(random, sampleRate) {
5130
+ if (random < Number(sampleRate)) return true;
5131
+ return false;
5360
5132
  };
5361
- function createBatchSender(config) {
5362
- var transport = config.transport;
5363
- var endpoint = config.endpoint, _a = config.size, size = _a === void 0 ? DEFAULT_SIZE : _a, _b = config.wait, wait = _b === void 0 ? DEFAULT_WAIT : _b;
5364
- var batch = [];
5365
- var tid = 0;
5366
- var fail;
5367
- var success;
5368
- var sender = {
5369
- getSize: function() {
5370
- return size;
5371
- },
5372
- getWait: function() {
5373
- return wait;
5374
- },
5375
- setSize: function(v) {
5376
- size = v;
5377
- },
5378
- setWait: function(v) {
5379
- wait = v;
5380
- },
5381
- getEndpoint: function() {
5382
- return endpoint;
5383
- },
5384
- setEndpoint: function(v) {
5385
- endpoint = v;
5386
- },
5387
- send: function(e) {
5388
- batch.push(e);
5389
- if (batch.length >= size) sendBatch.call(this);
5390
- clearTimeout(tid);
5391
- tid = setTimeout(sendBatch.bind(this), wait);
5392
- },
5393
- flush: function() {
5394
- clearTimeout(tid);
5395
- sendBatch.call(this);
5396
- },
5397
- getBatchData: function() {
5398
- return batch.length ? stringifyBatch(batch) : "";
5399
- },
5400
- clear: function() {
5401
- clearTimeout(tid);
5402
- batch = [];
5403
- },
5404
- fail: function(cb) {
5405
- fail = cb;
5406
- },
5407
- success: function(cb) {
5408
- success = cb;
5409
- }
5410
- };
5411
- function sendBatch() {
5412
- if (!batch.length) return;
5413
- var data = this.getBatchData();
5414
- transport.post({
5415
- url: endpoint,
5416
- data,
5417
- fail: function(err) {
5418
- fail && fail(err, data);
5419
- },
5420
- success: function() {
5421
- success && success(data);
5422
- }
5423
- });
5424
- batch = [];
5425
- }
5426
- return sender;
5427
- }
5428
- /*! *****************************************************************************
5429
- Copyright (c) Microsoft Corporation.
5430
-
5431
- Permission to use, copy, modify, and/or distribute this software for any
5432
- purpose with or without fee is hereby granted.
5433
-
5434
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
5435
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
5436
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
5437
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
5438
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
5439
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
5440
- PERFORMANCE OF THIS SOFTWARE.
5441
- ***************************************************************************** */
5442
- var __assign = function() {
5443
- __assign = Object.assign || function __assign(t) {
5444
- for (var s, i = 1, n = arguments.length; i < n; i++) {
5445
- s = arguments[i];
5446
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
5133
+ var runProcessors = function(fns) {
5134
+ return function(e) {
5135
+ var r = e;
5136
+ for (var i = 0; i < fns.length; i++) if (r) try {
5137
+ r = fns[i](r);
5138
+ } catch (err) {
5139
+ error(err);
5447
5140
  }
5448
- return t;
5141
+ else break;
5142
+ return r;
5449
5143
  };
5450
- return __assign.apply(this, arguments);
5451
5144
  };
5452
- function __values(o) {
5453
- var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
5454
- if (m) return m.call(o);
5455
- if (o && typeof o.length === "number") return { next: function() {
5456
- if (o && i >= o.length) o = void 0;
5457
- return {
5458
- value: o && o[i++],
5459
- done: !o
5460
- };
5461
- } };
5462
- throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
5463
- }
5464
- function __read(o, n) {
5465
- var m = typeof Symbol === "function" && o[Symbol.iterator];
5466
- if (!m) return o;
5467
- var i = m.call(o), r, ar = [], e;
5468
- try {
5469
- while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
5470
- } catch (error) {
5471
- e = { error };
5472
- } finally {
5473
- try {
5474
- if (r && !r.done && (m = i["return"])) m.call(i);
5475
- } finally {
5476
- if (e) throw e.error;
5477
- }
5478
- }
5479
- return ar;
5480
- }
5481
- function __spreadArray(to, from, pack) {
5482
- if (pack || arguments.length === 2) {
5483
- for (var i = 0, l = from.length, ar; i < l; i++) if (ar || !(i in from)) {
5484
- if (!ar) ar = Array.prototype.slice.call(from, 0, i);
5485
- ar[i] = from[i];
5486
- }
5145
+ /**
5146
+ * 生成uuid
5147
+ * stolen from https://github.com/kelektiv/node-uuid#readme uuid/v4
5148
+ *
5149
+ * @returns
5150
+ */
5151
+ function mathRNG() {
5152
+ var rnds = new Array(16);
5153
+ var r = 0;
5154
+ for (var i = 0; i < 16; i++) {
5155
+ if ((i & 3) === 0) r = Math.random() * 4294967296;
5156
+ rnds[i] = r >>> ((i & 3) << 3) & 255;
5487
5157
  }
5488
- return to.concat(ar || Array.prototype.slice.call(from));
5489
- }
5490
- var noop = function() {
5491
- return {};
5492
- };
5493
- function id(v) {
5494
- return v;
5495
- }
5496
- function isObject(o) {
5497
- return typeof o === "object" && o !== null;
5498
- }
5499
- var objProto = Object.prototype;
5500
- function isArray(o) {
5501
- return objProto.toString.call(o) === "[object Array]";
5502
- }
5503
- function isBoolean(o) {
5504
- return typeof o === "boolean";
5158
+ return rnds;
5505
5159
  }
5506
- function isNumber(o) {
5507
- return typeof o === "number";
5160
+ function bytesToUuid(buf) {
5161
+ var byteToHex = [];
5162
+ for (var index = 0; index < 256; ++index) byteToHex[index] = (index + 256).toString(16).substr(1);
5163
+ var i = 0;
5164
+ var bth = byteToHex;
5165
+ return [
5166
+ bth[buf[i++]],
5167
+ bth[buf[i++]],
5168
+ bth[buf[i++]],
5169
+ bth[buf[i++]],
5170
+ "-",
5171
+ bth[buf[i++]],
5172
+ bth[buf[i++]],
5173
+ "-",
5174
+ bth[buf[i++]],
5175
+ bth[buf[i++]],
5176
+ "-",
5177
+ bth[buf[i++]],
5178
+ bth[buf[i++]],
5179
+ "-",
5180
+ bth[buf[i++]],
5181
+ bth[buf[i++]],
5182
+ bth[buf[i++]],
5183
+ bth[buf[i++]],
5184
+ bth[buf[i++]],
5185
+ bth[buf[i++]]
5186
+ ].join("");
5508
5187
  }
5509
- function isString(o) {
5510
- return typeof o === "string";
5511
- }
5512
- function arrayIncludes(array, value) {
5513
- if (!isArray(array)) return false;
5514
- if (array.length === 0) return false;
5515
- var k = 0;
5516
- while (k < array.length) {
5517
- if (array[k] === value) return true;
5518
- k++;
5519
- }
5520
- return false;
5521
- }
5522
- var arrayRemove = function(arr, e) {
5523
- if (!isArray(arr)) return arr;
5524
- var i = arr.indexOf(e);
5525
- if (i >= 0) {
5526
- var arr_ = arr.slice();
5527
- arr_.splice(i, 1);
5528
- return arr_;
5529
- }
5530
- return arr;
5531
- };
5532
- /**
5533
- * 按路径访问对象属性
5534
- * @param target 待访问对象
5535
- * @param property 访问属性路径
5536
- * @param { (target: any, property: string): any } visitor 访问器
5537
- */
5538
- var safeVisit = function(target, path, visitor) {
5539
- var _a, _b;
5540
- var _c = __read(path.split(".")), method = _c[0], rest = _c.slice(1);
5541
- while (target && rest.length > 0) {
5542
- target = target[method];
5543
- _a = rest, _b = __read(_a), method = _b[0], rest = _b.slice(1);
5544
- }
5545
- if (!target) return;
5546
- return visitor(target, method);
5547
- };
5548
- function safeStringify(a) {
5549
- try {
5550
- return isString(a) ? a : JSON.stringify(a);
5551
- } catch (err) {
5552
- return "[FAILED_TO_STRINGIFY]:" + String(err);
5553
- }
5554
- }
5555
- function createContextAgent() {
5556
- var context = {};
5557
- var stringified = {};
5558
- var contextAgent = {
5559
- set: function(k, v) {
5560
- context[k] = v;
5561
- stringified[k] = safeStringify(v);
5562
- return contextAgent;
5563
- },
5564
- merge: function(ctx) {
5565
- context = __assign(__assign({}, context), ctx);
5566
- Object.keys(ctx).forEach(function(key) {
5567
- stringified[key] = safeStringify(ctx[key]);
5568
- });
5569
- return contextAgent;
5570
- },
5571
- delete: function(k) {
5572
- delete context[k];
5573
- delete stringified[k];
5574
- return contextAgent;
5575
- },
5576
- clear: function() {
5577
- context = {};
5578
- stringified = {};
5579
- return contextAgent;
5580
- },
5581
- get: function(k) {
5582
- return stringified[k];
5583
- },
5584
- toString: function() {
5585
- return __assign({}, stringified);
5586
- }
5587
- };
5588
- return contextAgent;
5589
- }
5590
- var getPrintString = function() {
5591
- if ("".padStart) return function(str, prefixLength) {
5592
- if (prefixLength === void 0) prefixLength = 8;
5593
- return str.padStart(prefixLength, " ");
5594
- };
5595
- return function(str) {
5596
- return str;
5597
- };
5598
- };
5599
- var printString = getPrintString();
5600
- var errCount = 0;
5601
- var error = function() {
5602
- var args = [];
5603
- for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
5604
- console.error.apply(console, __spreadArray([
5605
- "[SDK]",
5606
- Date.now(),
5607
- printString("" + errCount++)
5608
- ], __read(args), false));
5609
- };
5610
- var warnCount = 0;
5611
- var warn = function() {
5612
- var args = [];
5613
- for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
5614
- console.warn.apply(console, __spreadArray([
5615
- "[SDK]",
5616
- Date.now(),
5617
- printString("" + warnCount++)
5618
- ], __read(args), false));
5619
- };
5620
- var isHitBySampleRate = function(sampleRate) {
5621
- if (Math.random() < Number(sampleRate)) return true;
5622
- return false;
5623
- };
5624
- var isHitByRandom = function(random, sampleRate) {
5625
- if (random < Number(sampleRate)) return true;
5626
- return false;
5627
- };
5628
- var runProcessors = function(fns) {
5629
- return function(e) {
5630
- var r = e;
5631
- for (var i = 0; i < fns.length; i++) if (r) try {
5632
- r = fns[i](r);
5633
- } catch (err) {
5634
- error(err);
5635
- }
5636
- else break;
5637
- return r;
5638
- };
5639
- };
5640
- /**
5641
- * 生成uuid
5642
- * stolen from https://github.com/kelektiv/node-uuid#readme uuid/v4
5643
- *
5644
- * @returns
5645
- */
5646
- function mathRNG() {
5647
- var rnds = new Array(16);
5648
- var r = 0;
5649
- for (var i = 0; i < 16; i++) {
5650
- if ((i & 3) === 0) r = Math.random() * 4294967296;
5651
- rnds[i] = r >>> ((i & 3) << 3) & 255;
5652
- }
5653
- return rnds;
5654
- }
5655
- function bytesToUuid(buf) {
5656
- var byteToHex = [];
5657
- for (var index = 0; index < 256; ++index) byteToHex[index] = (index + 256).toString(16).substr(1);
5658
- var i = 0;
5659
- var bth = byteToHex;
5660
- return [
5661
- bth[buf[i++]],
5662
- bth[buf[i++]],
5663
- bth[buf[i++]],
5664
- bth[buf[i++]],
5665
- "-",
5666
- bth[buf[i++]],
5667
- bth[buf[i++]],
5668
- "-",
5669
- bth[buf[i++]],
5670
- bth[buf[i++]],
5671
- "-",
5672
- bth[buf[i++]],
5673
- bth[buf[i++]],
5674
- "-",
5675
- bth[buf[i++]],
5676
- bth[buf[i++]],
5677
- bth[buf[i++]],
5678
- bth[buf[i++]],
5679
- bth[buf[i++]],
5680
- bth[buf[i++]]
5681
- ].join("");
5682
- }
5683
- function uuid() {
5684
- var rnds = mathRNG();
5685
- rnds[6] = rnds[6] & 15 | 64;
5686
- rnds[8] = rnds[8] & 63 | 128;
5687
- return bytesToUuid(rnds);
5188
+ function uuid() {
5189
+ var rnds = mathRNG();
5190
+ rnds[6] = rnds[6] & 15 | 64;
5191
+ rnds[8] = rnds[8] & 63 | 128;
5192
+ return bytesToUuid(rnds);
5688
5193
  }
5689
5194
  function createDestroyAgent() {
5690
5195
  var destroyed = false;
@@ -6476,381 +5981,905 @@ function resolveSlardarClient(moduleValue) {
6476
5981
  }
6477
5982
  const Slardar = resolveSlardarClient(import_index_cjs.default);
6478
5983
  //#endregion
6479
- //#region ../../openclaw-slardar/lib/env.js
5984
+ //#region ../../openclaw-slardar/lib/env.js
5985
+ /**
5986
+ * 解析 Slardar 上报环境。
5987
+ *
5988
+ * 优先级:
5989
+ * 1. compile-time define `__SLARDAR_ENV__`(消费方 tsdown 配置注入)
5990
+ * 2. runtime env `process.env.MIAODA_SLARDAR_ENV`
5991
+ * 3. 默认 "online"(生产保守)
5992
+ */
5993
+ function getSlardarEnv() {
5994
+ return "online";
5995
+ }
5996
+ //#endregion
5997
+ //#region ../../openclaw-slardar/lib/init.js
5998
+ let initialized = false;
5999
+ let resolvedAppId;
6000
+ /**
6001
+ * 初始化 Slardar 上报通道。
6002
+ * 幂等:重复调用仅第一次生效;失败静默(不 rethrow)。
6003
+ *
6004
+ * 参数化设计:
6005
+ * - `bid` 由消费方传入(miaoda 家族统一用 "apaas_miaoda")
6006
+ * - `release` 由消费方读 package.json.version 传入
6007
+ * - `env` 可选,省略时走 `getSlardarEnv()`(compile-time define → process.env → "online")
6008
+ * - `appId` 可选,显式传入优先;否则读 `process.env.app_id`;都没就 undefined。
6009
+ * 通过 `getAppId()` 暴露给 `buildTelemetryFields` 自动注入到每次上报。
6010
+ */
6011
+ function initSlardar(opts) {
6012
+ if (initialized) return;
6013
+ resolvedAppId = opts.appId ?? process.env.app_id ?? void 0;
6014
+ try {
6015
+ Slardar.init?.({
6016
+ bid: opts.bid,
6017
+ release: opts.release,
6018
+ env: opts.env ?? getSlardarEnv(),
6019
+ transport: {
6020
+ get: ({ url, success, fail }) => {
6021
+ return fetch(url).then((response) => response.json()).then((data) => success?.(data)).catch((error) => fail?.(error));
6022
+ },
6023
+ post: ({ url, data }) => {
6024
+ const body = typeof data === "string" ? data : JSON.stringify(data);
6025
+ return fetch(url, {
6026
+ method: "POST",
6027
+ headers: { "Content-Type": "application/json" },
6028
+ body
6029
+ }).catch(() => {});
6030
+ }
6031
+ }
6032
+ });
6033
+ Slardar.start?.();
6034
+ initialized = true;
6035
+ } catch {}
6036
+ }
6037
+ /**
6038
+ * 读取 init 时捕获的 appId;未 init 或 init 时既没传 opts.appId 又没 process.env.app_id → undefined。
6039
+ * 由 `buildTelemetryFields` 在 params.appId 缺失时自动 fallback。
6040
+ */
6041
+ function getAppId() {
6042
+ return resolvedAppId;
6043
+ }
6044
+ //#endregion
6045
+ //#region ../../openclaw-slardar/lib/shared.js
6046
+ function toStringValue(value) {
6047
+ if (value === void 0) return void 0;
6048
+ return String(value);
6049
+ }
6050
+ function buildTelemetryFields(params) {
6051
+ const fields = { source: "node-plugin" };
6052
+ const traceId = params.traceId;
6053
+ const projectId = params.projectId;
6054
+ const isNew = toStringValue(params.isNew);
6055
+ const appId = params.appId ?? getAppId();
6056
+ const agentId = params.agentId;
6057
+ const sessionKey = params.sessionKey;
6058
+ if (traceId) fields.trace_id = traceId;
6059
+ if (projectId) fields.project_id = projectId;
6060
+ if (isNew) fields.is_new = isNew;
6061
+ if (appId) fields.app_id = appId;
6062
+ if (agentId) fields.agent_id = agentId;
6063
+ if (params.domain) fields.domain = params.domain;
6064
+ if (sessionKey) fields.session_key = sessionKey;
6065
+ return fields;
6066
+ }
6067
+ function mergeLogExtra(base, extra) {
6068
+ const merged = { ...base };
6069
+ for (const [key, value] of Object.entries(extra ?? {})) {
6070
+ if (value === void 0 || value === null) continue;
6071
+ merged[key] = typeof value === "boolean" ? String(value) : value;
6072
+ }
6073
+ return merged;
6074
+ }
6075
+ //#endregion
6076
+ //#region ../../openclaw-slardar/lib/report-task.js
6077
+ const DEFAULT_EVENT_NAME = "miaoda_coding_task";
6078
+ function reportTask(params) {
6079
+ try {
6080
+ const metrics = {};
6081
+ const telemetryFields = buildTelemetryFields(params);
6082
+ const durationMs = params.durationMs;
6083
+ const sseEventCount = params.sseEventCount;
6084
+ const reconnectCount = params.reconnectCount;
6085
+ const createDurationMs = params.createDurationMs;
6086
+ if (typeof durationMs === "number") metrics.duration_ms = durationMs;
6087
+ if (typeof sseEventCount === "number") metrics.sse_event_count = sseEventCount;
6088
+ if (typeof reconnectCount === "number") metrics.reconnect_count = reconnectCount;
6089
+ if (typeof createDurationMs === "number") metrics.create_duration_ms = createDurationMs;
6090
+ if (params.extraMetrics) {
6091
+ for (const [key, value] of Object.entries(params.extraMetrics)) if (typeof value === "number") metrics[key] = value;
6092
+ }
6093
+ const categories = {
6094
+ ...telemetryFields,
6095
+ is_new: telemetryFields.is_new ?? "false"
6096
+ };
6097
+ if (params.status) categories.status = params.status;
6098
+ const errorType = params.errorType;
6099
+ if (params.phase) categories.phase = params.phase;
6100
+ if (errorType) categories.error_type = errorType.slice(0, 100);
6101
+ if (params.extraCategories) {
6102
+ for (const [key, value] of Object.entries(params.extraCategories)) if (typeof value === "string") categories[key] = value;
6103
+ }
6104
+ Slardar.sendEvent?.({
6105
+ name: params.eventName ?? DEFAULT_EVENT_NAME,
6106
+ metrics,
6107
+ categories
6108
+ });
6109
+ } catch {}
6110
+ }
6111
+ //#endregion
6112
+ //#region ../../openclaw-slardar/lib/report-error.js
6113
+ function reportError(params) {
6114
+ try {
6115
+ if (!params.projectId && !params.sessionKey && !params.agentId) {}
6116
+ const extra = mergeLogExtra(buildTelemetryFields(params), {
6117
+ event: params.event,
6118
+ phase: params.phase
6119
+ });
6120
+ const logIds = params.logIds;
6121
+ if (logIds?.length) extra.log_ids = logIds.join(",");
6122
+ if (params.extraCategories) {
6123
+ for (const [key, value] of Object.entries(params.extraCategories)) if (typeof value === "string") extra[key] = value;
6124
+ }
6125
+ Slardar.sendLog?.({
6126
+ content: params.error,
6127
+ level: "error",
6128
+ extra
6129
+ });
6130
+ } catch {}
6131
+ }
6132
+ //#endregion
6133
+ //#region src/install-cli.ts
6134
+ const LARK_CLI_NAME = "lark-cli";
6135
+ const AGENT_SKILLS_NAME = "agent-skills";
6136
+ const WORKSPACE_AGENT_REL = "workspace/agent";
6137
+ async function installClis(tag, ossFileMap, opts) {
6138
+ const homeBase = resolveHomeBase(opts.homeBase);
6139
+ if (opts.names.length === 0) throw new Error("install-cli: must provide at least one --cli=<name>");
6140
+ const manifest = await fetchManifest(ossFileMap, tag);
6141
+ const allClis = manifest.packages.filter((p) => p.role === "cli" && p.name !== "openclaw");
6142
+ const wanted = new Set(opts.names);
6143
+ let targets = allClis.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
6144
+ const foundKeys = /* @__PURE__ */ new Set();
6145
+ for (const t of targets) {
6146
+ foundKeys.add(t.name);
6147
+ if (t.packageName) foundKeys.add(t.packageName);
6148
+ }
6149
+ const missing = opts.names.filter((n) => !foundKeys.has(n));
6150
+ if (missing.length > 0) throw new Error(`install-cli: not found in manifest: ${missing.join(", ")}`);
6151
+ const withUrl = [];
6152
+ const missingUrl = [];
6153
+ for (const p of targets) if (ossFileMap[p.ossKey]) withUrl.push(p);
6154
+ else missingUrl.push(p.ossKey);
6155
+ if (missingUrl.length > 0) console.error(`[install-cli] WARN: signed URL missing for [${missingUrl.join(", ")}] — skipping`);
6156
+ targets = withUrl;
6157
+ if (targets.length === 0) {
6158
+ console.error(`[install-cli] no installable targets after URL check, done`);
6159
+ return;
6160
+ }
6161
+ console.error(`[install-cli] tag=${tag} targets=${targets.length}`);
6162
+ const t0 = Date.now();
6163
+ const tarballs = await Promise.all(targets.map(async (p) => {
6164
+ const tb = await downloadWithCache(p, ossFileMap, opts);
6165
+ console.error(`[install-cli] ${p.name}: downloaded`);
6166
+ return {
6167
+ pkg: p,
6168
+ tarball: tb
6169
+ };
6170
+ }));
6171
+ for (const { pkg, tarball } of tarballs) {
6172
+ installOne(pkg, tarball, homeBase, opts.tmpRoot);
6173
+ linkBins(pkg, homeBase);
6174
+ console.error(`[install-cli] ${pkg.name}: installed`);
6175
+ }
6176
+ if (targets.some((p) => p.name === LARK_CLI_NAME)) {
6177
+ const skillsInstallPath = await installAgentSkills(manifest, ossFileMap, opts, homeBase, opts.tmpRoot);
6178
+ if (skillsInstallPath) linkAgentSkills(homeBase, skillsInstallPath);
6179
+ const appIds = collectFeishuAppIds();
6180
+ console.error(`[install-cli] lark-cli installed — running lark-cli-init for ${appIds.length} appId(s): [${appIds.join(", ")}]`);
6181
+ for (const appId of appIds) {
6182
+ console.error(`[install-cli] lark-cli-init: appId=${appId}`);
6183
+ const result = runLarkCliInit({
6184
+ appId,
6185
+ feishuAppSecret: opts.feishuAppSecret
6186
+ });
6187
+ console.error(`[install-cli] lark-cli-init: appId=${appId} ok=${result.ok}` + (result.skipped ? ` skipped=true reason=${result.skipReason}` : "") + (result.error ? ` error=${result.error}` : ""));
6188
+ const outputText = [
6189
+ result.error,
6190
+ result.configInitStdout,
6191
+ result.configInitStderr
6192
+ ].join(" ");
6193
+ if (/error/i.test(outputText)) reportError({
6194
+ error: `lark-cli-init failed for appId=${appId}: ${result.error ?? "unknown"}`,
6195
+ extraCategories: {
6196
+ event: "install_cli_lark_init_failed",
6197
+ appId
6198
+ }
6199
+ });
6200
+ }
6201
+ }
6202
+ console.error(`[install-cli] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
6203
+ }
6204
+ function linkBins(pkg, homeBase) {
6205
+ const targetDir = node_path.default.join(homeBase, pkg.installPath);
6206
+ const pkgJsonPath = node_path.default.join(targetDir, "package.json");
6207
+ let pkgJson;
6208
+ try {
6209
+ pkgJson = JSON.parse(node_fs.default.readFileSync(pkgJsonPath, "utf-8"));
6210
+ } catch (e) {
6211
+ console.error(`[install-cli] linkBins: could not read package.json for ${pkg.name}: ${e.message}`);
6212
+ return;
6213
+ }
6214
+ const { bin } = pkgJson;
6215
+ if (!bin) {
6216
+ console.error(`[install-cli] linkBins: no bin field in package.json for ${pkg.name}, skipping`);
6217
+ return;
6218
+ }
6219
+ const entries = typeof bin === "string" ? [[node_path.default.basename(typeof pkgJson.name === "string" ? pkgJson.name : pkg.name), bin]] : Object.entries(bin);
6220
+ const binDir = node_path.default.join(homeBase, ".npm-global/bin");
6221
+ node_fs.default.mkdirSync(binDir, { recursive: true });
6222
+ for (const [binName, binFile] of entries) {
6223
+ const binTarget = node_path.default.join(targetDir, binFile);
6224
+ try {
6225
+ node_fs.default.chmodSync(binTarget, 493);
6226
+ } catch {}
6227
+ const symlinkPath = node_path.default.join(binDir, binName);
6228
+ const relTarget = node_path.default.relative(binDir, binTarget);
6229
+ try {
6230
+ node_fs.default.unlinkSync(symlinkPath);
6231
+ } catch {}
6232
+ node_fs.default.symlinkSync(relTarget, symlinkPath);
6233
+ console.error(`[install-cli] linkBins: ${symlinkPath} -> ${relTarget}`);
6234
+ }
6235
+ }
6236
+ async function installAgentSkills(manifest, ossFileMap, downloadOpts, homeBase, tmpRoot) {
6237
+ const pkg = manifest.packages.find((p) => p.role === "template" && p.name === AGENT_SKILLS_NAME);
6238
+ if (!pkg) {
6239
+ console.error(`[install-cli] installAgentSkills: ${AGENT_SKILLS_NAME} not found in manifest (tag may not bundle it) — skipping skill install`);
6240
+ return null;
6241
+ }
6242
+ console.error(`[install-cli] installAgentSkills: downloading ${pkg.name}@${pkg.version}`);
6243
+ const tarball = await downloadWithCache(pkg, ossFileMap, downloadOpts);
6244
+ const targetDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, pkg.installPath);
6245
+ const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(tmpRoot ?? node_os.default.tmpdir(), "agent-skills-"));
6246
+ try {
6247
+ console.error(`[install-cli] installAgentSkills: extracting to tmpStage=${tmpStage}`);
6248
+ extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
6249
+ const extracted = node_fs.default.readdirSync(tmpStage);
6250
+ console.error(`[install-cli] installAgentSkills: extracted ${extracted.length} entries: [${extracted.join(", ")}]`);
6251
+ const bakDir = targetDir + ".bak";
6252
+ const newDir = targetDir + ".new";
6253
+ node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
6254
+ if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
6255
+ recursive: true,
6256
+ force: true
6257
+ });
6258
+ if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6259
+ recursive: true,
6260
+ force: true
6261
+ });
6262
+ moveSafe(tmpStage, newDir);
6263
+ const hadExisting = node_fs.default.existsSync(targetDir);
6264
+ console.error(`[install-cli] installAgentSkills: swapping into targetDir=${targetDir} hadExisting=${hadExisting}`);
6265
+ try {
6266
+ if (hadExisting) moveSafe(targetDir, bakDir);
6267
+ moveSafe(newDir, targetDir);
6268
+ } catch (e) {
6269
+ if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
6270
+ moveSafe(bakDir, targetDir);
6271
+ } catch {}
6272
+ try {
6273
+ node_fs.default.rmSync(newDir, {
6274
+ recursive: true,
6275
+ force: true
6276
+ });
6277
+ } catch {}
6278
+ throw e;
6279
+ }
6280
+ if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6281
+ recursive: true,
6282
+ force: true
6283
+ });
6284
+ console.error(`[install-cli] installAgentSkills: done — skills installed to ${targetDir}`);
6285
+ return pkg.installPath;
6286
+ } finally {
6287
+ if (node_fs.default.existsSync(tmpStage)) try {
6288
+ node_fs.default.rmSync(tmpStage, {
6289
+ recursive: true,
6290
+ force: true
6291
+ });
6292
+ } catch {}
6293
+ }
6294
+ }
6295
+ /**
6296
+ * (Re)create workspace/agent/skills/<name> → ../<installPath>/<name> symlinks so
6297
+ * the agent workspace sees agent-skills under the conventional skills/ directory.
6298
+ *
6299
+ * Both sides share workspace/agent/ as parent, so the relative link form is stable
6300
+ * regardless of where homeBase is mounted.
6301
+ *
6302
+ * @param homeBase Root directory (default /home/gem). installPath is relative to
6303
+ * <homeBase>/workspace/agent/.
6304
+ * @param installPath Relative path of the extracted skills dir under workspace/agent
6305
+ * (e.g. ".agents/skills").
6306
+ */
6307
+ function linkAgentSkills(homeBase, installPath) {
6308
+ const targetDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, installPath);
6309
+ const skillsLinkDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "skills");
6310
+ if (!node_fs.default.existsSync(targetDir)) {
6311
+ console.error(`[install-cli] linkAgentSkills: targetDir=${targetDir} does not exist — skipping`);
6312
+ return;
6313
+ }
6314
+ const skillDirs = node_fs.default.readdirSync(targetDir, { withFileTypes: true }).filter((e) => e.isDirectory());
6315
+ console.error(`[install-cli] linkAgentSkills: creating ${skillDirs.length} symlinks in ${skillsLinkDir}`);
6316
+ node_fs.default.mkdirSync(skillsLinkDir, { recursive: true });
6317
+ for (const entry of skillDirs) {
6318
+ const relTarget = node_path.default.join("..", installPath, entry.name);
6319
+ const symlinkPath = node_path.default.join(skillsLinkDir, entry.name);
6320
+ try {
6321
+ node_fs.default.unlinkSync(symlinkPath);
6322
+ } catch {}
6323
+ node_fs.default.symlinkSync(relTarget, symlinkPath);
6324
+ console.error(`[install-cli] linkAgentSkills: ${entry.name} -> ${relTarget}`);
6325
+ }
6326
+ console.error(`[install-cli] linkAgentSkills: done — ${skillDirs.length} skill(s) symlinked from ${skillsLinkDir}`);
6327
+ }
6328
+ function installOne(pkg, tarball, homeBase, tmpRoot) {
6329
+ const targetDir = node_path.default.join(homeBase, pkg.installPath);
6330
+ const bakDir = targetDir + ".bak";
6331
+ const newDir = targetDir + ".new";
6332
+ node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
6333
+ if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
6334
+ recursive: true,
6335
+ force: true
6336
+ });
6337
+ if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6338
+ recursive: true,
6339
+ force: true
6340
+ });
6341
+ const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(tmpRoot ?? node_os.default.tmpdir(), "cli-install-"));
6342
+ try {
6343
+ extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
6344
+ if (!node_fs.default.existsSync(node_path.default.join(tmpStage, "package.json"))) throw new Error(`cli tarball missing package.json: ${pkg.name}`);
6345
+ moveSafe(tmpStage, newDir);
6346
+ const hadExisting = node_fs.default.existsSync(targetDir);
6347
+ try {
6348
+ if (hadExisting) moveSafe(targetDir, bakDir);
6349
+ moveSafe(newDir, targetDir);
6350
+ } catch (e) {
6351
+ if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
6352
+ moveSafe(bakDir, targetDir);
6353
+ } catch {}
6354
+ try {
6355
+ node_fs.default.rmSync(newDir, {
6356
+ recursive: true,
6357
+ force: true
6358
+ });
6359
+ } catch {}
6360
+ throw e;
6361
+ }
6362
+ if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6363
+ recursive: true,
6364
+ force: true
6365
+ });
6366
+ } finally {
6367
+ if (node_fs.default.existsSync(tmpStage)) try {
6368
+ node_fs.default.rmSync(tmpStage, {
6369
+ recursive: true,
6370
+ force: true
6371
+ });
6372
+ } catch {}
6373
+ }
6374
+ }
6375
+ //#endregion
6376
+ //#region src/download-resource.ts
6480
6377
  /**
6481
- * 解析 Slardar 上报环境。
6378
+ * Download + extract a config/template package to its install destination.
6482
6379
  *
6483
- * 优先级:
6484
- * 1. compile-time define `__SLARDAR_ENV__`(消费方 tsdown 配置注入)
6485
- * 2. runtime env `process.env.MIAODA_SLARDAR_ENV`
6486
- * 3. 默认 "online"(生产保守)
6380
+ * Current manifest has all resources as format=tgz with content at the root
6381
+ * (config: openclaw.json file at root; template: scripts/ dir at root), so we
6382
+ * always `tar -xzf` without --strip-components into `dirname(fullInstallPath)`.
6383
+ * The final artefact ends up at exactly `homeBase + pkg.installPath`.
6487
6384
  */
6488
- function getSlardarEnv() {
6489
- return "online";
6385
+ async function downloadResource(tag, ossFileMap, opts) {
6386
+ const homeBase = resolveHomeBase(opts.homeBase);
6387
+ const pkg = (await fetchManifest(ossFileMap, tag)).packages.find((p) => p.role === opts.role && p.name === opts.name);
6388
+ if (!pkg) throw new Error(`download-resource: not found in manifest: role=${opts.role} name=${opts.name}`);
6389
+ const file = await downloadWithCache(pkg, ossFileMap, opts);
6390
+ const fullInstallPath = node_path.default.join(homeBase, pkg.installPath);
6391
+ const extractDir = opts.dir ?? node_path.default.dirname(fullInstallPath);
6392
+ node_fs.default.mkdirSync(extractDir, { recursive: true });
6393
+ const format = (pkg.format ?? "").toLowerCase();
6394
+ const lower = pkg.ossKey.toLowerCase();
6395
+ if (format === "tgz" || lower.endsWith(".tgz") || lower.endsWith(".tar.gz")) {
6396
+ extractTarballTolerant(file, extractDir);
6397
+ console.error(`[download-resource] ${opts.role}/${opts.name}: extracted to ${extractDir}`);
6398
+ } else {
6399
+ const basename = node_path.default.posix.basename(pkg.ossKey);
6400
+ node_fs.default.copyFileSync(file, node_path.default.join(extractDir, basename));
6401
+ console.error(`[download-resource] ${opts.role}/${opts.name}: copied ${basename} to ${extractDir}`);
6402
+ }
6490
6403
  }
6491
6404
  //#endregion
6492
- //#region ../../openclaw-slardar/lib/init.js
6493
- let initialized = false;
6494
- let resolvedAppId;
6405
+ //#region src/oss/getOpenclawTag.ts
6495
6406
  /**
6496
- * 初始化 Slardar 上报通道。
6497
- * 幂等:重复调用仅第一次生效;失败静默(不 rethrow)。
6407
+ * Extracts the openclaw tag from the manifest key present in ossFileMap.
6408
+ * Avoids passing an extra ctx field — we already know the tag from the
6409
+ * well-known manifest key studio_server included.
6498
6410
  *
6499
- * 参数化设计:
6500
- * - `bid` 由消费方传入(miaoda 家族统一用 "apaas_miaoda")
6501
- * - `release` 由消费方读 package.json.version 传入
6502
- * - `env` 可选,省略时走 `getSlardarEnv()`(compile-time define → process.env → "online")
6503
- * - `appId` 可选,显式传入优先;否则读 `process.env.app_id`;都没就 undefined。
6504
- * 通过 `getAppId()` 暴露给 `buildTelemetryFields` 自动注入到每次上报。
6505
- */
6506
- function initSlardar(opts) {
6507
- if (initialized) return;
6508
- resolvedAppId = opts.appId ?? process.env.app_id ?? void 0;
6509
- try {
6510
- Slardar.init?.({
6511
- bid: opts.bid,
6512
- release: opts.release,
6513
- env: opts.env ?? getSlardarEnv(),
6514
- transport: {
6515
- get: ({ url, success, fail }) => {
6516
- return fetch(url).then((response) => response.json()).then((data) => success?.(data)).catch((error) => fail?.(error));
6517
- },
6518
- post: ({ url, data }) => {
6519
- const body = typeof data === "string" ? data : JSON.stringify(data);
6520
- return fetch(url, {
6521
- method: "POST",
6522
- headers: { "Content-Type": "application/json" },
6523
- body
6524
- }).catch(() => {});
6525
- }
6526
- }
6527
- });
6528
- Slardar.start?.();
6529
- initialized = true;
6530
- } catch {}
6531
- }
6532
- /**
6533
- * 读取 init 时捕获的 appId;未 init 或 init 时既没传 opts.appId 又没 process.env.app_id → undefined。
6534
- * 由 `buildTelemetryFields` 在 params.appId 缺失时自动 fallback。
6411
+ * Manifest key shape: builtin/manifests/openclaw/recommended/<tag>.json
6535
6412
  */
6536
- function getAppId() {
6537
- return resolvedAppId;
6413
+ function getOpenclawTagFromOssFileMap(ossFileMap) {
6414
+ const prefix = "builtin/manifests/openclaw/recommended/";
6415
+ const suffix = ".json";
6416
+ for (const key of Object.keys(ossFileMap)) if (key.startsWith(prefix) && key.endsWith(suffix)) return key.slice(39, -5);
6417
+ throw new Error("cannot resolve openclaw tag: ossFileMap missing manifest key");
6538
6418
  }
6539
6419
  //#endregion
6540
- //#region ../../openclaw-slardar/lib/shared.js
6541
- function toStringValue(value) {
6542
- if (value === void 0) return void 0;
6543
- return String(value);
6544
- }
6545
- function buildTelemetryFields(params) {
6546
- const fields = { source: "node-plugin" };
6547
- const traceId = params.traceId;
6548
- const projectId = params.projectId;
6549
- const isNew = toStringValue(params.isNew);
6550
- const appId = params.appId ?? getAppId();
6551
- const agentId = params.agentId;
6552
- const sessionKey = params.sessionKey;
6553
- if (traceId) fields.trace_id = traceId;
6554
- if (projectId) fields.project_id = projectId;
6555
- if (isNew) fields.is_new = isNew;
6556
- if (appId) fields.app_id = appId;
6557
- if (agentId) fields.agent_id = agentId;
6558
- if (params.domain) fields.domain = params.domain;
6559
- if (sessionKey) fields.session_key = sessionKey;
6560
- return fields;
6420
+ //#region src/reset.ts
6421
+ const POLICY_KEYS = [
6422
+ "dmPolicy",
6423
+ "allowFrom",
6424
+ "groupPolicy",
6425
+ "groupAllowFrom"
6426
+ ];
6427
+ const STEPS = [
6428
+ "备份当前配置",
6429
+ "生成默认配置",
6430
+ "杀掉 openclaw 进程",
6431
+ "等待沙箱初始化完成",
6432
+ "确认 openclaw 版本",
6433
+ "合并核心备份配置",
6434
+ "检查启动脚本",
6435
+ "安装扩展和CLI工具",
6436
+ "启动并验证"
6437
+ ];
6438
+ const TOTAL_STEPS = STEPS.length;
6439
+ function writeResultFile(resultFile, result) {
6440
+ const dir = node_path.default.dirname(resultFile);
6441
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
6442
+ const tmpPath = resultFile + ".tmp";
6443
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(result), "utf-8");
6444
+ moveSafe(tmpPath, resultFile);
6561
6445
  }
6562
- function mergeLogExtra(base, extra) {
6563
- const merged = { ...base };
6564
- for (const [key, value] of Object.entries(extra ?? {})) {
6565
- if (value === void 0 || value === null) continue;
6566
- merged[key] = typeof value === "boolean" ? String(value) : value;
6567
- }
6568
- return merged;
6446
+ function updateProgress(resultFile, step, startedAt) {
6447
+ writeResultFile(resultFile, {
6448
+ status: "running",
6449
+ step,
6450
+ totalSteps: TOTAL_STEPS,
6451
+ progress: STEPS[step - 1],
6452
+ startedAt
6453
+ });
6569
6454
  }
6570
- //#endregion
6571
- //#region ../../openclaw-slardar/lib/report-task.js
6572
- const DEFAULT_EVENT_NAME = "miaoda_coding_task";
6573
- function reportTask(params) {
6574
- try {
6575
- const metrics = {};
6576
- const telemetryFields = buildTelemetryFields(params);
6577
- const durationMs = params.durationMs;
6578
- const sseEventCount = params.sseEventCount;
6579
- const reconnectCount = params.reconnectCount;
6580
- const createDurationMs = params.createDurationMs;
6581
- if (typeof durationMs === "number") metrics.duration_ms = durationMs;
6582
- if (typeof sseEventCount === "number") metrics.sse_event_count = sseEventCount;
6583
- if (typeof reconnectCount === "number") metrics.reconnect_count = reconnectCount;
6584
- if (typeof createDurationMs === "number") metrics.create_duration_ms = createDurationMs;
6585
- if (params.extraMetrics) {
6586
- for (const [key, value] of Object.entries(params.extraMetrics)) if (typeof value === "number") metrics[key] = value;
6587
- }
6588
- const categories = {
6589
- ...telemetryFields,
6590
- is_new: telemetryFields.is_new ?? "false"
6591
- };
6592
- if (params.status) categories.status = params.status;
6593
- const errorType = params.errorType;
6594
- if (params.phase) categories.phase = params.phase;
6595
- if (errorType) categories.error_type = errorType.slice(0, 100);
6596
- if (params.extraCategories) {
6597
- for (const [key, value] of Object.entries(params.extraCategories)) if (typeof value === "string") categories[key] = value;
6598
- }
6599
- Slardar.sendEvent?.({
6600
- name: params.eventName ?? DEFAULT_EVENT_NAME,
6601
- metrics,
6602
- categories
6603
- });
6604
- } catch {}
6455
+ function markDone(resultFile, startedAt) {
6456
+ writeResultFile(resultFile, {
6457
+ status: "done",
6458
+ step: TOTAL_STEPS,
6459
+ totalSteps: TOTAL_STEPS,
6460
+ progress: "重置完成",
6461
+ startedAt,
6462
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
6463
+ });
6605
6464
  }
6606
- //#endregion
6607
- //#region ../../openclaw-slardar/lib/report-error.js
6608
- function reportError(params) {
6609
- try {
6610
- if (!params.projectId && !params.sessionKey && !params.agentId) {}
6611
- const extra = mergeLogExtra(buildTelemetryFields(params), {
6612
- event: params.event,
6613
- phase: params.phase
6614
- });
6615
- const logIds = params.logIds;
6616
- if (logIds?.length) extra.log_ids = logIds.join(",");
6617
- if (params.extraCategories) {
6618
- for (const [key, value] of Object.entries(params.extraCategories)) if (typeof value === "string") extra[key] = value;
6619
- }
6620
- Slardar.sendLog?.({
6621
- content: params.error,
6622
- level: "error",
6623
- extra
6624
- });
6625
- } catch {}
6465
+ function markFailed(resultFile, step, error, startedAt) {
6466
+ writeResultFile(resultFile, {
6467
+ status: "failed",
6468
+ step,
6469
+ totalSteps: TOTAL_STEPS,
6470
+ progress: step > 0 ? STEPS[step - 1] : "初始化",
6471
+ error,
6472
+ startedAt,
6473
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
6474
+ });
6626
6475
  }
6627
- //#endregion
6628
- //#region src/install-cli.ts
6629
- const LARK_CLI_NAME = "lark-cli";
6630
- const AGENT_SKILLS_NAME = "agent-skills";
6631
- const WORKSPACE_AGENT_REL = "workspace/agent";
6632
- async function installClis(tag, ossFileMap, opts) {
6633
- const homeBase = resolveHomeBase(opts.homeBase);
6634
- if (opts.names.length === 0) throw new Error("install-cli: must provide at least one --cli=<name>");
6635
- const manifest = await fetchManifest(ossFileMap, tag);
6636
- const allClis = manifest.packages.filter((p) => p.role === "cli" && p.name !== "openclaw");
6637
- const wanted = new Set(opts.names);
6638
- const targets = allClis.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
6639
- const foundKeys = /* @__PURE__ */ new Set();
6640
- for (const t of targets) {
6641
- foundKeys.add(t.name);
6642
- if (t.packageName) foundKeys.add(t.packageName);
6643
- }
6644
- const missing = opts.names.filter((n) => !foundKeys.has(n));
6645
- if (missing.length > 0) throw new Error(`install-cli: not found in manifest: ${missing.join(", ")}`);
6646
- console.error(`[install-cli] tag=${tag} targets=${targets.length}`);
6647
- const t0 = Date.now();
6648
- const tarballs = await Promise.all(targets.map(async (p) => {
6649
- const tb = await downloadWithCache(p, ossFileMap, opts);
6650
- console.error(`[install-cli] ${p.name}: downloaded`);
6651
- return {
6652
- pkg: p,
6653
- tarball: tb
6654
- };
6655
- }));
6656
- for (const { pkg, tarball } of tarballs) {
6657
- installOne(pkg, tarball, homeBase, opts.tmpRoot);
6658
- linkBins(pkg, homeBase);
6659
- console.error(`[install-cli] ${pkg.name}: installed`);
6476
+ /**
6477
+ * Download the template assets (config/openclaw.json + template/scripts) from
6478
+ * OSS into a scratch directory so the existing step 2 (generateDefaultConfig)
6479
+ * and step 7 (copyStartupScripts) can consume them as local files — the rest
6480
+ * of the orchestrator code stays untouched.
6481
+ *
6482
+ * Called once before step 1. The caller is responsible for rm -rf'ing
6483
+ * stagedDir in a finally{} block after the reset completes (or fails).
6484
+ */
6485
+ async function stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log) {
6486
+ if (node_fs.default.existsSync(stagedDir)) node_fs.default.rmSync(stagedDir, {
6487
+ recursive: true,
6488
+ force: true
6489
+ });
6490
+ node_fs.default.mkdirSync(stagedDir, { recursive: true });
6491
+ await downloadResource(openclawTag, ossFileMap, {
6492
+ role: "config",
6493
+ name: "openclaw.json",
6494
+ dir: stagedDir
6495
+ });
6496
+ await downloadResource(openclawTag, ossFileMap, {
6497
+ role: "template",
6498
+ name: "scripts",
6499
+ dir: configDir
6500
+ });
6501
+ log(`staged openclaw.json to ${stagedDir}, scripts directly to ${configDir}/scripts`);
6502
+ }
6503
+ /** Step 1: Backup current config as openclaw.json.bak.N */
6504
+ function backupCurrentConfig(configPath, log) {
6505
+ if (!fileExists(configPath)) {
6506
+ log("no existing config, skip backup");
6507
+ return;
6660
6508
  }
6661
- if (targets.some((p) => p.name === LARK_CLI_NAME)) {
6662
- const skillsInstallPath = await installAgentSkills(manifest, ossFileMap, opts, homeBase, opts.tmpRoot);
6663
- if (skillsInstallPath) linkAgentSkills(homeBase, skillsInstallPath);
6664
- const appIds = collectFeishuAppIds();
6665
- console.error(`[install-cli] lark-cli installed — running lark-cli-init for ${appIds.length} appId(s): [${appIds.join(", ")}]`);
6666
- for (const appId of appIds) {
6667
- console.error(`[install-cli] lark-cli-init: appId=${appId}`);
6668
- const result = runLarkCliInit({
6669
- appId,
6670
- feishuAppSecret: opts.feishuAppSecret
6671
- });
6672
- console.error(`[install-cli] lark-cli-init: appId=${appId} ok=${result.ok}` + (result.skipped ? ` skipped=true reason=${result.skipReason}` : "") + (result.error ? ` error=${result.error}` : ""));
6673
- const outputText = [
6674
- result.error,
6675
- result.configInitStdout,
6676
- result.configInitStderr
6677
- ].join(" ");
6678
- if (/error/i.test(outputText)) reportError({
6679
- error: `lark-cli-init failed for appId=${appId}: ${result.error ?? "unknown"}`,
6680
- extraCategories: {
6681
- event: "install_cli_lark_init_failed",
6682
- appId
6683
- }
6684
- });
6509
+ const dir = node_path.default.dirname(configPath);
6510
+ let maxN = 0;
6511
+ try {
6512
+ for (const f of node_fs.default.readdirSync(dir)) {
6513
+ const match = f.match(/^openclaw\.json\.bak\.(\d+)$/);
6514
+ if (match) {
6515
+ const n = parseInt(match[1], 10);
6516
+ if (n > maxN) maxN = n;
6517
+ }
6685
6518
  }
6519
+ } catch {}
6520
+ const bakPath = configPath + ".bak." + (maxN + 1);
6521
+ node_fs.default.copyFileSync(configPath, bakPath);
6522
+ log(`backed up to ${bakPath}`);
6523
+ }
6524
+ /** Step 2: Replace $$__XXX__ placeholders and write default config. */
6525
+ function generateDefaultConfig(srcDir, configPath, templateVars, log) {
6526
+ const srcConfigPath = node_path.default.join(srcDir, "openclaw.json");
6527
+ if (!fileExists(srcConfigPath)) throw new Error("staged openclaw.json not found at " + srcConfigPath);
6528
+ let content = node_fs.default.readFileSync(srcConfigPath, "utf-8");
6529
+ let replaced = 0;
6530
+ for (const [placeholder, value] of Object.entries(templateVars)) {
6531
+ const parts = content.split(placeholder);
6532
+ if (parts.length > 1) replaced += parts.length - 1;
6533
+ content = parts.join(value);
6686
6534
  }
6687
- console.error(`[install-cli] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
6535
+ node_fs.default.writeFileSync(configPath, content, "utf-8");
6536
+ log(`wrote ${configPath} (${replaced} placeholder(s) replaced, ${Object.keys(templateVars).length} provided)`);
6688
6537
  }
6689
- function linkBins(pkg, homeBase) {
6690
- const targetDir = node_path.default.join(homeBase, pkg.installPath);
6691
- const pkgJsonPath = node_path.default.join(targetDir, "package.json");
6692
- let pkgJson;
6538
+ /** Step 3: Kill all openclaw processes. */
6539
+ function killOpenclawProcesses(log) {
6693
6540
  try {
6694
- pkgJson = JSON.parse(node_fs.default.readFileSync(pkgJsonPath, "utf-8"));
6695
- } catch (e) {
6696
- console.error(`[install-cli] linkBins: could not read package.json for ${pkg.name}: ${e.message}`);
6697
- return;
6698
- }
6699
- const { bin } = pkgJson;
6700
- if (!bin) {
6701
- console.error(`[install-cli] linkBins: no bin field in package.json for ${pkg.name}, skipping`);
6702
- return;
6703
- }
6704
- const entries = typeof bin === "string" ? [[node_path.default.basename(typeof pkgJson.name === "string" ? pkgJson.name : pkg.name), bin]] : Object.entries(bin);
6705
- const binDir = node_path.default.join(homeBase, ".npm-global/bin");
6706
- node_fs.default.mkdirSync(binDir, { recursive: true });
6707
- for (const [binName, binFile] of entries) {
6708
- const binTarget = node_path.default.join(targetDir, binFile);
6541
+ shell("pkill -f openclaw-gateway || true", 5e3);
6542
+ } catch {}
6543
+ shell("sleep 2", 5e3);
6544
+ log("killed openclaw-gateway processes");
6545
+ }
6546
+ /**
6547
+ * Step 4: Wait for the sandbox's own init (init_sandbox.sh / concurrent npm
6548
+ * install) to finish before we start our own work. Two processes sharing
6549
+ * ~/.npm cache + competing for disk/network just makes everything crawl;
6550
+ * letting init finish first is the cleanest way to get exclusive access.
6551
+ * Polls every 10s up to `maxWaitMs`. If the deadline is hit we fall through
6552
+ * anyway better to try than to fail the reset outright.
6553
+ *
6554
+ * Kept even after we switched off `npm install` because the sandbox init
6555
+ * script still runs `npm install` for other packages and holds cache locks.
6556
+ */
6557
+ function waitForInitNpm(maxWaitMs, log) {
6558
+ const deadline = Date.now() + maxWaitMs;
6559
+ const ownPid = String(process.pid);
6560
+ let polls = 0;
6561
+ while (Date.now() < deadline) {
6562
+ polls++;
6563
+ let running = 0;
6709
6564
  try {
6710
- node_fs.default.chmodSync(binTarget, 493);
6711
- } catch {}
6712
- const symlinkPath = node_path.default.join(binDir, binName);
6713
- const relTarget = node_path.default.relative(binDir, binTarget);
6565
+ const out = shell(`pgrep -af "init_sandbox.sh|npm install|npm i " | grep -v -- "${ownPid}" | wc -l`, 1e4);
6566
+ running = parseInt(out.trim(), 10) || 0;
6567
+ } catch {
6568
+ log(`poll ${polls}: no concurrent npm, proceeding`);
6569
+ return;
6570
+ }
6571
+ if (running === 0) {
6572
+ log(`poll ${polls}: no concurrent npm, proceeding`);
6573
+ return;
6574
+ }
6575
+ log(`poll ${polls}: ${running} concurrent npm/init process(es) still running, waiting 10s`);
6714
6576
  try {
6715
- node_fs.default.unlinkSync(symlinkPath);
6577
+ shell("sleep 10", 12e3);
6716
6578
  } catch {}
6717
- node_fs.default.symlinkSync(relTarget, symlinkPath);
6718
- console.error(`[install-cli] linkBins: ${symlinkPath} -> ${relTarget}`);
6719
6579
  }
6580
+ log(`deadline (${maxWaitMs}ms) hit after ${polls} poll(s), proceeding anyway`);
6720
6581
  }
6721
- async function installAgentSkills(manifest, ossFileMap, downloadOpts, homeBase, tmpRoot) {
6722
- const pkg = manifest.packages.find((p) => p.role === "template" && p.name === AGENT_SKILLS_NAME);
6723
- if (!pkg) {
6724
- console.error(`[install-cli] installAgentSkills: ${AGENT_SKILLS_NAME} not found in manifest (tag may not bundle it) — skipping skill install`);
6725
- return null;
6726
- }
6727
- console.error(`[install-cli] installAgentSkills: downloading ${pkg.name}@${pkg.version}`);
6728
- const tarball = await downloadWithCache(pkg, ossFileMap, downloadOpts);
6729
- const targetDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, pkg.installPath);
6730
- const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(tmpRoot ?? node_os.default.tmpdir(), "agent-skills-"));
6731
- try {
6732
- console.error(`[install-cli] installAgentSkills: extracting to tmpStage=${tmpStage}`);
6733
- extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
6734
- const extracted = node_fs.default.readdirSync(tmpStage);
6735
- console.error(`[install-cli] installAgentSkills: extracted ${extracted.length} entries: [${extracted.join(", ")}]`);
6736
- const bakDir = targetDir + ".bak";
6737
- const newDir = targetDir + ".new";
6738
- node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
6739
- if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
6740
- recursive: true,
6741
- force: true
6742
- });
6743
- if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6744
- recursive: true,
6745
- force: true
6746
- });
6747
- moveSafe(tmpStage, newDir);
6748
- const hadExisting = node_fs.default.existsSync(targetDir);
6749
- console.error(`[install-cli] installAgentSkills: swapping into targetDir=${targetDir} hadExisting=${hadExisting}`);
6750
- try {
6751
- if (hadExisting) moveSafe(targetDir, bakDir);
6752
- moveSafe(newDir, targetDir);
6753
- } catch (e) {
6754
- if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
6755
- moveSafe(bakDir, targetDir);
6756
- } catch {}
6757
- try {
6758
- node_fs.default.rmSync(newDir, {
6759
- recursive: true,
6760
- force: true
6582
+ /**
6583
+ * Step 5: Install openclaw from the OSS-provided tarball at the target tag,
6584
+ * then verify `openclaw --version` output contains that tag. No npm involved.
6585
+ */
6586
+ async function step5InstallOpenclaw(openclawTag, ossFileMap, log) {
6587
+ log(`install-openclaw tag=${openclawTag}`);
6588
+ await installOpenclaw(openclawTag, ossFileMap);
6589
+ const out = shell("openclaw --version 2>&1 || true", 1e4).trim();
6590
+ if (!out.includes(openclawTag)) throw new Error(`openclaw version verify failed: got "${out}"`);
6591
+ log(`openclaw version verified: ${out}`);
6592
+ }
6593
+ /** Step 6: Merge coreBackup from resetData + ensure allowedOrigins. */
6594
+ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
6595
+ const JSON5 = loadJSON5();
6596
+ const backup = resetData.coreBackup;
6597
+ if (backup) {
6598
+ const config = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
6599
+ const merged = [];
6600
+ const backupAccounts = backup.channels?.feishu?.accounts;
6601
+ const isMultiAgent = (backup.agents?.length ?? 0) > 0 || backupAccounts != null && Object.keys(backupAccounts).length > 0;
6602
+ if (backup.bindings && backup.bindings.length > 0) {
6603
+ config.bindings = backup.bindings;
6604
+ merged.push("bindings");
6605
+ }
6606
+ if (isMultiAgent) {
6607
+ if (backup.agents && backup.agents.length > 0) {
6608
+ if (!config.agents) config.agents = {};
6609
+ const agents = config.agents;
6610
+ if (!Array.isArray(agents.list)) agents.list = [];
6611
+ const configDir = node_path.default.dirname(configPath);
6612
+ for (const agent of backup.agents) agents.list.push({
6613
+ id: agent.id,
6614
+ name: agent.id,
6615
+ workspace: agent.workspace,
6616
+ agentDir: configDir + "/agents/" + agent.id + "/agent"
6761
6617
  });
6762
- } catch {}
6763
- throw e;
6618
+ merged.push(`agents(+${backup.agents.length})`);
6619
+ const list = agents.list;
6620
+ let mainIdx = list.findIndex((a) => a.id === "main");
6621
+ if (mainIdx < 0) {
6622
+ list.unshift({ id: "main" });
6623
+ mainIdx = 0;
6624
+ }
6625
+ list[mainIdx].subagents = { allowAgents: ["*"] };
6626
+ list[mainIdx].default = true;
6627
+ merged.push("main-team-mode");
6628
+ }
6629
+ if (!config.channels) config.channels = {};
6630
+ const ch = config.channels;
6631
+ if (!ch.feishu) ch.feishu = {};
6632
+ const feishu = ch.feishu;
6633
+ if (!feishu.accounts) feishu.accounts = {};
6634
+ const accounts = feishu.accounts;
6635
+ if (backupAccounts && Object.keys(backupAccounts).length > 0) {
6636
+ Object.assign(accounts, backupAccounts);
6637
+ merged.push("channels.feishu.accounts");
6638
+ }
6639
+ if (typeof feishu.appId === "string" && feishu.appId !== "") {
6640
+ const topAppId = feishu.appId;
6641
+ const botKey = `bot-${topAppId}`;
6642
+ const existing = asRecord(accounts[botKey]) ?? {};
6643
+ if (!existing.appId) existing.appId = topAppId;
6644
+ if (!existing.appSecret && feishu.appSecret !== void 0) existing.appSecret = feishu.appSecret;
6645
+ for (const k of POLICY_KEYS) if (feishu[k] !== void 0 && !(k in existing)) existing[k] = feishu[k];
6646
+ accounts[botKey] = existing;
6647
+ delete feishu.appId;
6648
+ delete feishu.appSecret;
6649
+ for (const k of POLICY_KEYS) delete feishu[k];
6650
+ merged.push(`feishu.appId→${botKey}`);
6651
+ if (!Array.isArray(config.bindings)) config.bindings = [];
6652
+ const bindingList = config.bindings;
6653
+ if (!bindingList.some((b) => b.agentId === "main" && asRecord(b.match)?.channel === "feishu")) {
6654
+ bindingList.unshift({
6655
+ agentId: "main",
6656
+ match: {
6657
+ channel: "feishu",
6658
+ accountId: botKey
6659
+ }
6660
+ });
6661
+ merged.push(`binding:main→${botKey}`);
6662
+ }
6663
+ }
6664
+ delete accounts.default;
6665
+ delete accounts.defaultAccount;
6764
6666
  }
6765
- if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6766
- recursive: true,
6767
- force: true
6768
- });
6769
- console.error(`[install-cli] installAgentSkills: done — skills installed to ${targetDir}`);
6770
- return pkg.installPath;
6771
- } finally {
6772
- if (node_fs.default.existsSync(tmpStage)) try {
6773
- node_fs.default.rmSync(tmpStage, {
6774
- recursive: true,
6775
- force: true
6776
- });
6777
- } catch {}
6667
+ node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6668
+ log(`merged from coreBackup: [${merged.join(", ") || "nothing"}]`);
6669
+ } else log("no coreBackup in resetData, skip multi-agent merge");
6670
+ const expectedOrigins = Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
6671
+ if (expectedOrigins.length === 0) {
6672
+ log("no expectedOrigins provided");
6673
+ return;
6674
+ }
6675
+ const config = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
6676
+ if (!config.gateway) config.gateway = {};
6677
+ const gw = config.gateway;
6678
+ if (!gw.controlUi) gw.controlUi = {};
6679
+ const cui = gw.controlUi;
6680
+ const current = Array.isArray(cui.allowedOrigins) ? cui.allowedOrigins.filter((o) => typeof o === "string") : [];
6681
+ if (current.includes("*")) {
6682
+ log("allowedOrigins already contains \"*\", skip origin merge");
6683
+ return;
6684
+ }
6685
+ const seen = new Set(current);
6686
+ const added = [];
6687
+ const mergedOrigins = [...current];
6688
+ for (const o of expectedOrigins) if (!seen.has(o)) {
6689
+ mergedOrigins.push(o);
6690
+ seen.add(o);
6691
+ added.push(o);
6778
6692
  }
6693
+ cui.allowedOrigins = mergedOrigins;
6694
+ node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6695
+ log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
6779
6696
  }
6780
6697
  /**
6781
- * (Re)create workspace/agent/skills/<name> ../<installPath>/<name> symlinks so
6782
- * the agent workspace sees agent-skills under the conventional skills/ directory.
6698
+ * Step 7: Verify startup scripts landed in configDir/scripts/.
6783
6699
  *
6784
- * Both sides share workspace/agent/ as parent, so the relative link form is stable
6785
- * regardless of where homeBase is mounted.
6700
+ * Scripts are extracted directly to configDir/scripts/ during stageTemplate
6701
+ * there's no intermediate copy any more. This step is now a verification gate
6702
+ * (rather than a copy action) so the step count stays at 9 and we fail early
6703
+ * if the template tgz didn't carry a scripts/ dir.
6704
+ */
6705
+ function verifyStartupScripts(configDir, log) {
6706
+ const targetScriptsDir = node_path.default.join(configDir, "scripts");
6707
+ if (!node_fs.default.existsSync(targetScriptsDir)) throw new Error(`scripts dir missing at ${targetScriptsDir} — template download failed?`);
6708
+ log(`scripts dir present at ${targetScriptsDir}`);
6709
+ }
6710
+ /**
6711
+ * Step 8: Install all extensions AND CLI tools listed in the OSS manifest at
6712
+ * `openclawTag`. Extensions use installExtension (role=extension); CLI tools
6713
+ * use installClis (role=cli, excluding 'openclaw' which has its own step 5).
6786
6714
  *
6787
- * @param homeBase Root directory (default /home/gem). installPath is relative to
6788
- * <homeBase>/workspace/agent/.
6789
- * @param installPath Relative path of the extracted skills dir under workspace/agent
6790
- * (e.g. ".agents/skills").
6715
+ * skipConfigUpdate=true for extensions: reset regenerates openclaw.json via
6716
+ * template (step 2) + merges backup/origins (step 6), so splicing
6717
+ * plugins.installs here would be overwritten. CLI tools don't touch
6718
+ * openclaw.json at all.
6719
+ *
6720
+ * feishuAppSecret is passed through for lark-cli post-install init
6721
+ * (lark-cli config init --app-secret-stdin). Absent/empty is fine — installClis
6722
+ * treats it as optional and falls back to config-stored secret.
6791
6723
  */
6792
- function linkAgentSkills(homeBase, installPath) {
6793
- const targetDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, installPath);
6794
- const skillsLinkDir = node_path.default.join(homeBase, WORKSPACE_AGENT_REL, "skills");
6795
- if (!node_fs.default.existsSync(targetDir)) {
6796
- console.error(`[install-cli] linkAgentSkills: targetDir=${targetDir} does not exist — skipping`);
6797
- return;
6724
+ async function step8InstallAll(openclawTag, ossFileMap, feishuAppSecret, log) {
6725
+ log(`install-extension --all tag=${openclawTag}`);
6726
+ await installExtension(openclawTag, ossFileMap, {
6727
+ all: true,
6728
+ skipConfigUpdate: true
6729
+ });
6730
+ log("extensions installed");
6731
+ const cliNames = (await fetchManifest(ossFileMap, openclawTag)).packages.filter((p) => p.role === "cli" && p.name !== "openclaw").map((p) => p.name);
6732
+ if (cliNames.length > 0) {
6733
+ log(`install-cli names=[${cliNames.join(",")}] tag=${openclawTag}`);
6734
+ await installClis(openclawTag, ossFileMap, {
6735
+ names: cliNames,
6736
+ feishuAppSecret
6737
+ });
6738
+ log(`cli tools installed: ${cliNames.join(",")}`);
6739
+ } else log("no cli packages in manifest, skip");
6740
+ }
6741
+ /** Step 9: Write secrets/provider key files and restart openclaw. */
6742
+ function writeSecretsAndRestart(vars, resetData, configDir, log) {
6743
+ if (resetData.secretsContent && vars.secretsFilePath) {
6744
+ writeFile(vars.secretsFilePath, resetData.secretsContent);
6745
+ log(`wrote secrets to ${vars.secretsFilePath}`);
6798
6746
  }
6799
- const skillDirs = node_fs.default.readdirSync(targetDir, { withFileTypes: true }).filter((e) => e.isDirectory());
6800
- console.error(`[install-cli] linkAgentSkills: creating ${skillDirs.length} symlinks in ${skillsLinkDir}`);
6801
- node_fs.default.mkdirSync(skillsLinkDir, { recursive: true });
6802
- for (const entry of skillDirs) {
6803
- const relTarget = node_path.default.join("..", installPath, entry.name);
6804
- const symlinkPath = node_path.default.join(skillsLinkDir, entry.name);
6805
- try {
6806
- node_fs.default.unlinkSync(symlinkPath);
6807
- } catch {}
6808
- node_fs.default.symlinkSync(relTarget, symlinkPath);
6809
- console.error(`[install-cli] linkAgentSkills: ${entry.name} -> ${relTarget}`);
6747
+ if (resetData.providerKeyContent && vars.providerFilePath) {
6748
+ writeFile(vars.providerFilePath, resetData.providerKeyContent);
6749
+ log(`wrote provider key to ${vars.providerFilePath}`);
6810
6750
  }
6811
- console.error(`[install-cli] linkAgentSkills: done — ${skillDirs.length} skill(s) symlinked from ${skillsLinkDir}`);
6751
+ const restartScript = "/opt/force/bin/openclaw_scripts/restart.sh";
6752
+ if (fileExists(restartScript)) {
6753
+ const t = Date.now();
6754
+ shell(`bash '${restartScript}'`, 3e4);
6755
+ log(`restart.sh done in ${Date.now() - t}ms`);
6756
+ } else log(`no restart.sh at ${restartScript}, skip`);
6812
6757
  }
6813
- function installOne(pkg, tarball, homeBase, tmpRoot) {
6814
- const targetDir = node_path.default.join(homeBase, pkg.installPath);
6815
- const bakDir = targetDir + ".bak";
6816
- const newDir = targetDir + ".new";
6817
- node_fs.default.mkdirSync(node_path.default.dirname(targetDir), { recursive: true });
6818
- if (node_fs.default.existsSync(newDir)) node_fs.default.rmSync(newDir, {
6819
- recursive: true,
6820
- force: true
6758
+ async function runReset(input, taskId, resultFile) {
6759
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6760
+ const { configPath, vars, resetData } = input;
6761
+ const configDir = node_path.default.dirname(configPath);
6762
+ const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
6763
+ let currentStep = 0;
6764
+ let stepStartedAt = Date.now();
6765
+ const stepResults = [];
6766
+ const log = makeLogger(resetLogFile(taskId));
6767
+ log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
6768
+ log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
6769
+ log(`vars: feishuAppID=${vars.feishuAppID || "(empty)"} feishuAppSecret=${vars.feishuAppSecret ? "***" : "(empty)"} expectedOrigins=${JSON.stringify(vars.expectedOrigins ?? [])} recommendedTag=${vars.recommendedOpenclawTag ?? "(none)"}`);
6770
+ const ossFileMap = resetData.ossFileMap;
6771
+ if (!ossFileMap || Object.keys(ossFileMap).length === 0) {
6772
+ const err = "resetData.ossFileMap missing or empty";
6773
+ log(`ERROR: ${err}`);
6774
+ markFailed(resultFile, 0, err, startedAt);
6775
+ return {
6776
+ success: false,
6777
+ steps: stepResults,
6778
+ error: err,
6779
+ failedStep: 0
6780
+ };
6781
+ }
6782
+ let openclawTag;
6783
+ if (resetData.openclawTag) openclawTag = resetData.openclawTag;
6784
+ else try {
6785
+ openclawTag = getOpenclawTagFromOssFileMap(ossFileMap);
6786
+ } catch (e) {
6787
+ const err = e.message;
6788
+ log(`ERROR: ${err}`);
6789
+ markFailed(resultFile, 0, err, startedAt);
6790
+ return {
6791
+ success: false,
6792
+ steps: stepResults,
6793
+ error: err,
6794
+ failedStep: 0
6795
+ };
6796
+ }
6797
+ log(`openclawTag=${openclawTag}, ossFileMap keys=${Object.keys(ossFileMap).length}`);
6798
+ const cb = resetData.coreBackup;
6799
+ log(`coreBackup: ${cb ? `agents=${cb.agents?.length ?? 0} bindings=${cb.bindings?.length ?? 0} feishuAccounts=${Object.keys(cb.channels?.feishu?.accounts ?? {}).join(", ") || "(none)"}` : "none"}`);
6800
+ process.on("uncaughtException", (err) => {
6801
+ log(`FATAL uncaughtException: ${err.message}\n${err.stack ?? ""}`);
6802
+ markFailed(resultFile, currentStep, `uncaught exception: ${err.message}`, startedAt);
6803
+ process.exit(1);
6821
6804
  });
6822
- if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6823
- recursive: true,
6824
- force: true
6805
+ process.on("unhandledRejection", (reason) => {
6806
+ log(`FATAL unhandledRejection: ${String(reason)}`);
6807
+ markFailed(resultFile, currentStep, `unhandled rejection: ${reason}`, startedAt);
6808
+ process.exit(1);
6825
6809
  });
6826
- const tmpStage = node_fs.default.mkdtempSync(node_path.default.join(tmpRoot ?? node_os.default.tmpdir(), "cli-install-"));
6827
- try {
6828
- extractTarballTolerant(tarball, tmpStage, { stripComponents: 1 });
6829
- if (!node_fs.default.existsSync(node_path.default.join(tmpStage, "package.json"))) throw new Error(`cli tarball missing package.json: ${pkg.name}`);
6830
- moveSafe(tmpStage, newDir);
6831
- const hadExisting = node_fs.default.existsSync(targetDir);
6832
- try {
6833
- if (hadExisting) moveSafe(targetDir, bakDir);
6834
- moveSafe(newDir, targetDir);
6835
- } catch (e) {
6836
- if (hadExisting && !node_fs.default.existsSync(targetDir) && node_fs.default.existsSync(bakDir)) try {
6837
- moveSafe(bakDir, targetDir);
6838
- } catch {}
6839
- try {
6840
- node_fs.default.rmSync(newDir, {
6841
- recursive: true,
6842
- force: true
6843
- });
6844
- } catch {}
6845
- throw e;
6810
+ /** Advance to the next step, recording the previous step's success
6811
+ * duration and updating the on-disk progress file. */
6812
+ const step = (n) => {
6813
+ if (currentStep > 0) {
6814
+ const dur = Date.now() - stepStartedAt;
6815
+ log(`step ${currentStep} "${STEPS[currentStep - 1]}" done in ${dur}ms`);
6816
+ stepResults.push({
6817
+ step: currentStep,
6818
+ name: STEPS[currentStep - 1],
6819
+ status: "ok",
6820
+ durationMs: dur
6821
+ });
6846
6822
  }
6847
- if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
6848
- recursive: true,
6849
- force: true
6823
+ currentStep = n;
6824
+ stepStartedAt = Date.now();
6825
+ log(`--- step ${n}/${TOTAL_STEPS}: ${STEPS[n - 1]} ---`);
6826
+ updateProgress(resultFile, n, startedAt);
6827
+ };
6828
+ try {
6829
+ await stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log);
6830
+ step(1);
6831
+ backupCurrentConfig(configPath, log);
6832
+ step(2);
6833
+ generateDefaultConfig(stagedDir, configPath, resetData.templateVars, log);
6834
+ step(3);
6835
+ killOpenclawProcesses(log);
6836
+ step(4);
6837
+ waitForInitNpm(10 * 6e4, log);
6838
+ step(5);
6839
+ await step5InstallOpenclaw(openclawTag, ossFileMap, log);
6840
+ step(6);
6841
+ mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
6842
+ step(7);
6843
+ verifyStartupScripts(configDir, log);
6844
+ step(8);
6845
+ await step8InstallAll(openclawTag, ossFileMap, vars.feishuAppSecret || void 0, log);
6846
+ step(9);
6847
+ writeSecretsAndRestart(vars, resetData, configDir, log);
6848
+ const lastDur = Date.now() - stepStartedAt;
6849
+ log(`step 9 "${STEPS[8]}" done in ${lastDur}ms`);
6850
+ stepResults.push({
6851
+ step: 9,
6852
+ name: STEPS[8],
6853
+ status: "ok",
6854
+ durationMs: lastDur
6855
+ });
6856
+ log("=== reset completed successfully ===");
6857
+ markDone(resultFile, startedAt);
6858
+ return {
6859
+ success: true,
6860
+ steps: stepResults
6861
+ };
6862
+ } catch (e) {
6863
+ const err = e.message;
6864
+ const dur = Date.now() - stepStartedAt;
6865
+ log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${dur}ms: ${err}\n${e.stack ?? ""}`);
6866
+ stepResults.push({
6867
+ step: currentStep,
6868
+ name: STEPS[currentStep - 1] ?? "init",
6869
+ status: "fail",
6870
+ durationMs: dur,
6871
+ error: err
6850
6872
  });
6873
+ markFailed(resultFile, currentStep, err, startedAt);
6874
+ return {
6875
+ success: false,
6876
+ steps: stepResults,
6877
+ error: err,
6878
+ failedStep: currentStep
6879
+ };
6851
6880
  } finally {
6852
- if (node_fs.default.existsSync(tmpStage)) try {
6853
- node_fs.default.rmSync(tmpStage, {
6881
+ try {
6882
+ node_fs.default.rmSync(stagedDir, {
6854
6883
  recursive: true,
6855
6884
  force: true
6856
6885
  });
@@ -6858,6 +6887,44 @@ function installOne(pkg, tarball, homeBase, tmpRoot) {
6858
6887
  }
6859
6888
  }
6860
6889
  //#endregion
6890
+ //#region src/get-reset-task.ts
6891
+ /**
6892
+ * Long-poll for a reset task result.
6893
+ * Reads the result file every 1s for up to 30s.
6894
+ * Returns immediately on terminal states (done/failed).
6895
+ */
6896
+ function getResetTask(taskId) {
6897
+ const resultFile = resetResultFile(taskId);
6898
+ const deadline = Date.now() + 3e4;
6899
+ while (Date.now() < deadline) {
6900
+ if (!node_fs.default.existsSync(resultFile)) {
6901
+ sleepSync(1e3);
6902
+ continue;
6903
+ }
6904
+ try {
6905
+ const content = node_fs.default.readFileSync(resultFile, "utf-8");
6906
+ const result = JSON.parse(content);
6907
+ if (result.status === "done" || result.status === "failed") return result;
6908
+ } catch {}
6909
+ sleepSync(1e3);
6910
+ }
6911
+ if (node_fs.default.existsSync(resultFile)) try {
6912
+ return JSON.parse(node_fs.default.readFileSync(resultFile, "utf-8"));
6913
+ } catch {}
6914
+ return {
6915
+ status: "running",
6916
+ progress: "等待中..."
6917
+ };
6918
+ }
6919
+ /**
6920
+ * Synchronous sleep using Atomics.wait on a shared buffer.
6921
+ */
6922
+ function sleepSync(ms) {
6923
+ const buf = new SharedArrayBuffer(4);
6924
+ const arr = new Int32Array(buf);
6925
+ Atomics.wait(arr, 0, 0, ms);
6926
+ }
6927
+ //#endregion
6861
6928
  //#region src/oss/resolveOssFileMap.ts
6862
6929
  /**
6863
6930
  * Pick an OssFileMap in the order of decreasing specificity:
@@ -10038,7 +10105,7 @@ async function reportCliRun(opts) {
10038
10105
  //#region src/help.ts
10039
10106
  const BIN = "mclaw-diagnose";
10040
10107
  function versionBanner() {
10041
- return `v0.1.11`;
10108
+ return `v0.1.12-alpha.0`;
10042
10109
  }
10043
10110
  const COMMANDS = [
10044
10111
  {