@andyqiu/codeforge 0.3.10 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9007,7 +9007,7 @@ var handler5 = autoLearningServer;
9007
9007
 
9008
9008
  // plugins/channels.ts
9009
9009
  init_opencode_plugin_helpers();
9010
- import { promises as fs2 } from "node:fs";
9010
+ import { promises as fs2, statSync as statSync2 } from "node:fs";
9011
9011
  import * as path4 from "node:path";
9012
9012
 
9013
9013
  // lib/channels.ts
@@ -9285,6 +9285,13 @@ function transformSlackToWebhook(ch, ev) {
9285
9285
  text: { type: "plain_text", text: titleText.slice(0, 150), emoji: true }
9286
9286
  }
9287
9287
  ];
9288
+ const sevLabel = ch.show_severity_label !== false ? severityToLabel(ev.severity) : "";
9289
+ if (sevLabel) {
9290
+ blocks.push({
9291
+ type: "section",
9292
+ text: { type: "mrkdwn", text: sevLabel }
9293
+ });
9294
+ }
9288
9295
  if (message.rendered.trim()) {
9289
9296
  blocks.push({
9290
9297
  type: "section",
@@ -9297,6 +9304,56 @@ function transformSlackToWebhook(ch, ev) {
9297
9304
  elements: [{ type: "mrkdwn", text: mentionText }]
9298
9305
  });
9299
9306
  }
9307
+ const showFields = ch.show_fields ?? [...DEFAULT_SHOW_FIELDS];
9308
+ const labels = { ...DEFAULT_FIELD_LABELS, ...ch.field_labels ?? {} };
9309
+ const fieldLines = pickFieldLines(ev.data, showFields, labels);
9310
+ if (fieldLines.length > 0) {
9311
+ blocks.push({ type: "divider" });
9312
+ blocks.push({
9313
+ type: "section",
9314
+ text: { type: "mrkdwn", text: fieldLines.join(`
9315
+ `) }
9316
+ });
9317
+ }
9318
+ if (ev.severity !== undefined && ev.severity >= 20) {
9319
+ const errMsg = ev.data?.["error"];
9320
+ const stack = ev.data?.["stack"];
9321
+ if (typeof errMsg === "string" && errMsg) {
9322
+ let errContent = `**❌ Error**: ${errMsg.slice(0, 500)}`;
9323
+ if (typeof stack === "string" && stack) {
9324
+ const stackHead = stack.split(`
9325
+ `).slice(0, 5).join(`
9326
+ `);
9327
+ errContent += "\n```\n" + stackHead.slice(0, 1000) + "\n```";
9328
+ }
9329
+ blocks.push({ type: "divider" });
9330
+ blocks.push({
9331
+ type: "section",
9332
+ text: { type: "mrkdwn", text: errContent }
9333
+ });
9334
+ }
9335
+ }
9336
+ const sessionUrl = ev.data?.["session_url"];
9337
+ const logsUrl = ev.data?.["logs_url"];
9338
+ const slackActions = [];
9339
+ if (isValidCtaUrl(sessionUrl)) {
9340
+ slackActions.push({
9341
+ type: "button",
9342
+ text: { type: "plain_text", text: "查看会话", emoji: true },
9343
+ url: sessionUrl,
9344
+ style: "primary"
9345
+ });
9346
+ }
9347
+ if (isValidCtaUrl(logsUrl)) {
9348
+ slackActions.push({
9349
+ type: "button",
9350
+ text: { type: "plain_text", text: "查看日志", emoji: true },
9351
+ url: logsUrl
9352
+ });
9353
+ }
9354
+ if (slackActions.length > 0) {
9355
+ blocks.push({ type: "actions", elements: slackActions });
9356
+ }
9300
9357
  const footerParts = [`event=\`${ev.event}\``];
9301
9358
  if (ev.session_id)
9302
9359
  footerParts.push(`session=\`${ev.session_id.slice(0, 8)}\``);
@@ -9387,6 +9444,13 @@ function transformLarkToWebhook(ch, ev) {
9387
9444
  }).join(" ");
9388
9445
  const headerTemplate = severityToLarkHeader(ev.severity);
9389
9446
  const elements = [];
9447
+ const sevLabel = ch.show_severity_label !== false ? severityToLabel(ev.severity) : "";
9448
+ if (sevLabel) {
9449
+ elements.push({
9450
+ tag: "div",
9451
+ text: { tag: "lark_md", content: sevLabel }
9452
+ });
9453
+ }
9390
9454
  if (messageText || mentionMarkdown) {
9391
9455
  const fullMsg = [messageText, mentionMarkdown].filter(Boolean).join(`
9392
9456
 
@@ -9396,6 +9460,57 @@ function transformLarkToWebhook(ch, ev) {
9396
9460
  text: { tag: "lark_md", content: fullMsg.slice(0, 3000) }
9397
9461
  });
9398
9462
  }
9463
+ const showFields = ch.show_fields ?? [...DEFAULT_SHOW_FIELDS];
9464
+ const labels = { ...DEFAULT_FIELD_LABELS, ...ch.field_labels ?? {} };
9465
+ const fieldLines = pickFieldLines(ev.data, showFields, labels);
9466
+ if (fieldLines.length > 0) {
9467
+ elements.push({ tag: "hr" });
9468
+ elements.push({
9469
+ tag: "div",
9470
+ text: { tag: "lark_md", content: fieldLines.join(`
9471
+ `) }
9472
+ });
9473
+ }
9474
+ if (ev.severity !== undefined && ev.severity >= 20) {
9475
+ const errMsg = ev.data?.["error"];
9476
+ const stack = ev.data?.["stack"];
9477
+ if (typeof errMsg === "string" && errMsg) {
9478
+ let errContent = `**❌ Error**: ${errMsg.slice(0, 500)}`;
9479
+ if (typeof stack === "string" && stack) {
9480
+ const stackHead = stack.split(`
9481
+ `).slice(0, 5).join(`
9482
+ `);
9483
+ errContent += "\n```\n" + stackHead.slice(0, 1000) + "\n```";
9484
+ }
9485
+ elements.push({ tag: "hr" });
9486
+ elements.push({
9487
+ tag: "div",
9488
+ text: { tag: "lark_md", content: errContent }
9489
+ });
9490
+ }
9491
+ }
9492
+ const sessionUrl = ev.data?.["session_url"];
9493
+ const logsUrl = ev.data?.["logs_url"];
9494
+ const larkActions = [];
9495
+ if (isValidCtaUrl(sessionUrl)) {
9496
+ larkActions.push({
9497
+ tag: "button",
9498
+ text: { tag: "plain_text", content: "查看会话" },
9499
+ type: "primary",
9500
+ url: sessionUrl
9501
+ });
9502
+ }
9503
+ if (isValidCtaUrl(logsUrl)) {
9504
+ larkActions.push({
9505
+ tag: "button",
9506
+ text: { tag: "plain_text", content: "查看日志" },
9507
+ type: "default",
9508
+ url: logsUrl
9509
+ });
9510
+ }
9511
+ if (larkActions.length > 0) {
9512
+ elements.push({ tag: "action", actions: larkActions });
9513
+ }
9399
9514
  const footer = [
9400
9515
  `**event**: \`${ev.event}\``,
9401
9516
  ev.session_id ? `**session**: \`${ev.session_id.slice(0, 8)}\`` : null,
@@ -9431,6 +9546,60 @@ function severityToLarkHeader(sev) {
9431
9546
  return "blue";
9432
9547
  return "grey";
9433
9548
  }
9549
+ function severityToLabel(sev) {
9550
+ if (sev === undefined)
9551
+ return "";
9552
+ if (sev >= 40)
9553
+ return "\uD83D\uDEA8 CRITICAL";
9554
+ if (sev >= 30)
9555
+ return "\uD83D\uDD34 ERROR";
9556
+ if (sev >= 20)
9557
+ return "\uD83D\uDFE0 WARN";
9558
+ if (sev >= 10)
9559
+ return "\uD83D\uDD35 INFO";
9560
+ return "";
9561
+ }
9562
+ var DEFAULT_SHOW_FIELDS = [
9563
+ "agent",
9564
+ "model",
9565
+ "duration_ms",
9566
+ "cost",
9567
+ "files_changed",
9568
+ "status"
9569
+ ];
9570
+ var DEFAULT_FIELD_LABELS = {
9571
+ agent: "Agent",
9572
+ model: "Model",
9573
+ duration_ms: "耗时",
9574
+ cost: "费用",
9575
+ files_changed: "改动文件数",
9576
+ status: "状态",
9577
+ error: "错误"
9578
+ };
9579
+ function pickFieldLines(data, showFields, labels) {
9580
+ if (!data)
9581
+ return [];
9582
+ const out = [];
9583
+ for (const key of showFields) {
9584
+ const v = data[key];
9585
+ if (v === undefined || v === null || v === "")
9586
+ continue;
9587
+ const label = labels[key] ?? key;
9588
+ let valueStr;
9589
+ if (key === "duration_ms" && typeof v === "number") {
9590
+ valueStr = v >= 60000 ? `${(v / 60000).toFixed(1)}m` : `${(v / 1000).toFixed(1)}s`;
9591
+ } else if (key === "cost" && typeof v === "number") {
9592
+ valueStr = `$${v.toFixed(4)}`;
9593
+ } else {
9594
+ valueStr = String(v);
9595
+ }
9596
+ out.push(`**${label}**: ${valueStr}`);
9597
+ }
9598
+ return out;
9599
+ }
9600
+ function isValidCtaUrl(url) {
9601
+ return typeof url === "string" && url.startsWith("https://");
9602
+ }
9434
9603
  function computeLarkSign(secret, timestampSec) {
9435
9604
  const stringToSign = `${timestampSec}
9436
9605
  ${secret}`;
@@ -9524,11 +9693,88 @@ function makeChannelEvent(event, data = {}) {
9524
9693
  }
9525
9694
 
9526
9695
  // plugins/channels.ts
9696
+ init_global_config();
9527
9697
  var PLUGIN_NAME6 = "channels";
9528
9698
  logLifecycle(PLUGIN_NAME6, "import", {});
9529
9699
  var fallbackLog = makePluginLogger(PLUGIN_NAME6);
9530
9700
  var _channelsCache = null;
9531
- function loadChannelsFromEnv() {
9701
+ var _activatedDirectory;
9702
+ var KNOWN_TYPES = new Set([
9703
+ "webhook",
9704
+ "file",
9705
+ "exec",
9706
+ "kh",
9707
+ "mcp",
9708
+ "slack",
9709
+ "lark"
9710
+ ]);
9711
+ var REQUIRED_FIELDS = {
9712
+ webhook: ["url"],
9713
+ file: ["path"],
9714
+ exec: [],
9715
+ kh: [],
9716
+ mcp: ["server", "tool"],
9717
+ slack: ["webhook_url"],
9718
+ lark: ["webhook_url"]
9719
+ };
9720
+ function safePeek(o) {
9721
+ const out = {};
9722
+ for (const k of ["type", "name"]) {
9723
+ if (k in o)
9724
+ out[k] = o[k];
9725
+ }
9726
+ return out;
9727
+ }
9728
+ function isChannel(x, log4) {
9729
+ if (!x || typeof x !== "object")
9730
+ return false;
9731
+ const o = x;
9732
+ const t = o["type"];
9733
+ const n = o["name"];
9734
+ if (typeof n !== "string" || n.length === 0) {
9735
+ log4?.warn(`[channels] 配置被忽略:缺少 name 字段`, { sample: safePeek(o) });
9736
+ return false;
9737
+ }
9738
+ if (typeof t !== "string" || !KNOWN_TYPES.has(t)) {
9739
+ log4?.warn(`[channels] 配置 '${n}' 被忽略:未知 type='${String(t)}',合法值=${[...KNOWN_TYPES].join(",")}`);
9740
+ return false;
9741
+ }
9742
+ const required = REQUIRED_FIELDS[t] ?? [];
9743
+ for (const f of required) {
9744
+ const v = o[f];
9745
+ if (typeof v !== "string" || v.length === 0) {
9746
+ log4?.warn(`[channels] 配置 '${n}' (type=${t}) 被忽略:缺少必填字段 '${f}'`);
9747
+ return false;
9748
+ }
9749
+ }
9750
+ if (t === "exec") {
9751
+ const hasArgv = Array.isArray(o["argv"]) && o["argv"].length > 0;
9752
+ const hasCmd = typeof o["command"] === "string" && o["command"].length > 0;
9753
+ if (!hasArgv && !hasCmd) {
9754
+ log4?.warn(`[channels] 配置 '${n}' (type=exec) 被忽略:argv 与 command 均为空`);
9755
+ return false;
9756
+ }
9757
+ }
9758
+ return true;
9759
+ }
9760
+ function resolveGlobalConfigPath() {
9761
+ return path4.join(globalConfigDir(), "channels.json");
9762
+ }
9763
+ function resolveProjectConfigPaths(root) {
9764
+ const r = root ?? _activatedDirectory ?? process.cwd();
9765
+ return {
9766
+ recommended: path4.join(projectConfigDir(r), "channels.json"),
9767
+ legacy: path4.join(projectConfigDir(r), "config", "channels.json")
9768
+ };
9769
+ }
9770
+ var _warnedKeys2 = new Set;
9771
+ function warnOnce3(key, msg, log4) {
9772
+ if (_warnedKeys2.has(key))
9773
+ return;
9774
+ _warnedKeys2.add(key);
9775
+ log4.warn(msg);
9776
+ }
9777
+ function loadChannelsFromEnv(log4 = fallbackLog) {
9532
9778
  const raw = process.env.CODEFORGE_CHANNELS_JSON;
9533
9779
  if (!raw)
9534
9780
  return [];
@@ -9536,22 +9782,103 @@ function loadChannelsFromEnv() {
9536
9782
  const parsed = JSON.parse(raw);
9537
9783
  if (!Array.isArray(parsed))
9538
9784
  return [];
9539
- return parsed.filter((c) => isChannel(c));
9785
+ return parsed.filter((c) => isChannel(c, log4));
9540
9786
  } catch {
9787
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON JSON 解析失败,忽略整段 env 配置`);
9541
9788
  return [];
9542
9789
  }
9543
9790
  }
9544
- function isChannel(x) {
9545
- if (!x || typeof x !== "object")
9546
- return false;
9547
- const t = x.type;
9548
- const n = x.name;
9549
- return typeof n === "string" && (t === "webhook" || t === "file" || t === "exec" || t === "kh" || t === "mcp");
9791
+ function loadChannelsFromGlobal(log4 = fallbackLog) {
9792
+ const filePath = resolveGlobalConfigPath();
9793
+ const raw = loadJsonIfExists(filePath);
9794
+ if (!raw)
9795
+ return [];
9796
+ try {
9797
+ const st = statSync2(filePath);
9798
+ const perm = st.mode & 511;
9799
+ if ((perm & 63) !== 0) {
9800
+ warnOnce3(`global-perm:${filePath}`, `[channels] 全局 channels.json 权限 0${perm.toString(8)} 含 group/other 可读位,` + `含 webhook 时建议 chmod 600 ${filePath}`, log4);
9801
+ }
9802
+ } catch {}
9803
+ const arr = Array.isArray(raw) ? raw : raw.channels;
9804
+ if (!Array.isArray(arr))
9805
+ return [];
9806
+ return arr.filter((c) => isChannel(c, log4));
9807
+ }
9808
+ function loadChannelsFromFile(root, log4 = fallbackLog) {
9809
+ const { recommended, legacy } = resolveProjectConfigPaths(root);
9810
+ let raw = loadJsonIfExists(recommended);
9811
+ if (raw === null) {
9812
+ const legacyRaw = loadJsonIfExists(legacy);
9813
+ if (legacyRaw !== null) {
9814
+ log4.warn(`[channels] 检测到 0.3.10 兼容路径 ${legacy},建议迁移到 0.3.11 推荐路径 ${recommended}` + `(与 KH 配置惯例统一)。两条路径同时存在时只读推荐路径。`);
9815
+ raw = legacyRaw;
9816
+ }
9817
+ }
9818
+ if (!raw)
9819
+ return [];
9820
+ const arr = Array.isArray(raw) ? raw : raw.channels;
9821
+ if (!Array.isArray(arr))
9822
+ return [];
9823
+ return arr.filter((c) => isChannel(c, log4));
9824
+ }
9825
+ function parseRawEnvLength(raw) {
9826
+ if (raw === undefined || raw === "")
9827
+ return "not-set";
9828
+ try {
9829
+ const p = JSON.parse(raw);
9830
+ return Array.isArray(p) ? p.length : "invalid";
9831
+ } catch {
9832
+ return "invalid";
9833
+ }
9550
9834
  }
9551
- function ensureChannels() {
9835
+ function mergeByName(layers, log4) {
9836
+ const map = new Map;
9837
+ for (const { layer, items } of layers) {
9838
+ for (const c of items) {
9839
+ const prev = map.get(c.name);
9840
+ if (prev) {
9841
+ const typeNote = prev.channel.type !== c.type ? `(type: ${prev.channel.type} → ${c.type})` : "";
9842
+ log4.warn(`[channels] channel '${c.name}' (${layer} 层) 覆盖 ${prev.layer} 层同名配置${typeNote}`);
9843
+ }
9844
+ map.set(c.name, { layer, channel: c });
9845
+ }
9846
+ }
9847
+ return [...map.values()].map((e) => e.channel);
9848
+ }
9849
+ function ensureChannels(log4 = fallbackLog) {
9552
9850
  if (_channelsCache)
9553
9851
  return _channelsCache;
9554
- _channelsCache = loadChannelsFromEnv();
9852
+ const rawEnv = process.env.CODEFORGE_CHANNELS_JSON;
9853
+ const rawLen = parseRawEnvLength(rawEnv);
9854
+ if (rawLen === 0) {
9855
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON 显式为空数组,跳过 global + project 两层(全部通知已禁用)`);
9856
+ _channelsCache = [];
9857
+ return _channelsCache;
9858
+ }
9859
+ const fromGlobal = loadChannelsFromGlobal(log4);
9860
+ const fromFile = loadChannelsFromFile(undefined, log4);
9861
+ const fromEnv = rawLen === "not-set" ? [] : loadChannelsFromEnv(log4);
9862
+ if (rawLen === "not-set") {
9863
+ _channelsCache = mergeByName([
9864
+ { layer: "global", items: fromGlobal },
9865
+ { layer: "project", items: fromFile }
9866
+ ], log4);
9867
+ return _channelsCache;
9868
+ }
9869
+ if (fromEnv.length === 0) {
9870
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON 含 ${rawLen === "invalid" ? "非数组" : rawLen} 项但全部被过滤,` + `退回 global + project 两层(global ${fromGlobal.length} 项 + project ${fromFile.length} 项)`);
9871
+ _channelsCache = mergeByName([
9872
+ { layer: "global", items: fromGlobal },
9873
+ { layer: "project", items: fromFile }
9874
+ ], log4);
9875
+ return _channelsCache;
9876
+ }
9877
+ _channelsCache = mergeByName([
9878
+ { layer: "global", items: fromGlobal },
9879
+ { layer: "project", items: fromFile },
9880
+ { layer: "env", items: fromEnv }
9881
+ ], log4);
9555
9882
  return _channelsCache;
9556
9883
  }
9557
9884
  var _limiter = null;
@@ -9698,6 +10025,7 @@ var TRIGGER_EVENT_TYPES3 = new Set([
9698
10025
  "subtasks.completed"
9699
10026
  ]);
9700
10027
  var channelsServer = async (ctx) => {
10028
+ _activatedDirectory = ctx.directory;
9701
10029
  logLifecycle(PLUGIN_NAME6, "activate", {
9702
10030
  directory: ctx.directory,
9703
10031
  triggerEventTypes: [...TRIGGER_EVENT_TYPES3],
@@ -12211,6 +12539,11 @@ import { z as z26 } from "zod";
12211
12539
  // lib/model-config.ts
12212
12540
  import { promises as fs7 } from "node:fs";
12213
12541
  import * as path10 from "node:path";
12542
+
12543
+ // lib/model-tier.ts
12544
+ var TIER_ORDER = ["quick", "balanced", "deep", "ultra"];
12545
+
12546
+ // lib/model-config.ts
12214
12547
  var CONFIG_FILE = "codeforge.json";
12215
12548
  var DEFAULT_RUNTIME_FALLBACK = {
12216
12549
  enabled: true,
@@ -12315,11 +12648,29 @@ function validateConfig(input) {
12315
12648
  return { ok: false, warnings, error: r.error };
12316
12649
  runtime = r.cfg;
12317
12650
  }
12651
+ let tiers;
12652
+ if (obj.tiers !== undefined) {
12653
+ const r = normalizeTiers(obj.tiers);
12654
+ if (!r.ok)
12655
+ return { ok: false, warnings, error: r.error };
12656
+ tiers = r.cfg;
12657
+ }
12318
12658
  for (const [name, agent] of Object.entries(agents)) {
12319
12659
  if (agent.category && !categories?.[agent.category]) {
12320
12660
  warnings.push(`agent[${name}].category="${agent.category}" 未在 categories 定义,将忽略 category 链`);
12321
12661
  }
12322
12662
  }
12663
+ for (const [name, agent] of Object.entries(agents)) {
12664
+ if (!agent.tier)
12665
+ continue;
12666
+ const mappedCat = tiers?.category_map?.[agent.tier];
12667
+ const hasOverride = agent.tier_overrides?.[agent.tier] !== undefined;
12668
+ if (!mappedCat && !hasOverride) {
12669
+ warnings.push(`agent[${name}].tier="${agent.tier}" 既无 models.tiers.category_map["${agent.tier}"] 映射,` + `也无 tier_overrides["${agent.tier}"];该 agent 将不参与 tier 体系(adapter 返 null)。`);
12670
+ } else if (mappedCat && !categories?.[mappedCat] && !hasOverride) {
12671
+ warnings.push(`agent[${name}].tier="${agent.tier}" 通过 category_map 映射到 "${mappedCat}",` + `但 categories.${mappedCat} 不存在;该 agent 将不参与 tier 体系。`);
12672
+ }
12673
+ }
12323
12674
  return {
12324
12675
  ok: true,
12325
12676
  warnings,
@@ -12328,7 +12679,8 @@ function validateConfig(input) {
12328
12679
  _doc: typeof obj._doc === "string" ? obj._doc : undefined,
12329
12680
  agents,
12330
12681
  categories,
12331
- runtime_fallback: runtime
12682
+ runtime_fallback: runtime,
12683
+ tiers
12332
12684
  }
12333
12685
  };
12334
12686
  }
@@ -12349,6 +12701,23 @@ function normalizeAgent(name, raw) {
12349
12701
  const thinking = o.thinking !== undefined ? normalizeThinking(`agent[${name}]`, o.thinking) : undefined;
12350
12702
  if (thinking && !thinking.ok)
12351
12703
  return { ok: false, error: thinking.error };
12704
+ let tier;
12705
+ if (o.tier !== undefined) {
12706
+ if (typeof o.tier !== "string" || !isValidTierLevel(o.tier)) {
12707
+ return {
12708
+ ok: false,
12709
+ error: `agent[${name}].tier="${String(o.tier)}" 不是合法 TierLevel (期望 ${TIER_ORDER.join("/")})`
12710
+ };
12711
+ }
12712
+ tier = o.tier;
12713
+ }
12714
+ let tierOverrides;
12715
+ if (o.tier_overrides !== undefined) {
12716
+ const r = normalizeTierOverrides(name, o.tier_overrides);
12717
+ if (!r.ok)
12718
+ return { ok: false, error: r.error };
12719
+ tierOverrides = r.value;
12720
+ }
12352
12721
  return {
12353
12722
  ok: true,
12354
12723
  binding: {
@@ -12357,6 +12726,8 @@ function normalizeAgent(name, raw) {
12357
12726
  category: typeof o.category === "string" ? o.category : undefined,
12358
12727
  thinking: thinking?.value,
12359
12728
  fallback_models: fallbacks.value,
12729
+ tier,
12730
+ tier_overrides: tierOverrides,
12360
12731
  _doc: typeof o._doc === "string" ? o._doc : undefined
12361
12732
  }
12362
12733
  };
@@ -12453,6 +12824,99 @@ function normalizeRuntime(raw) {
12453
12824
  cfg._doc = o._doc;
12454
12825
  return { ok: true, cfg };
12455
12826
  }
12827
+ function isValidTierLevel(x) {
12828
+ return typeof x === "string" && TIER_ORDER.includes(x);
12829
+ }
12830
+ function normalizeTierOverrides(agentName, raw) {
12831
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12832
+ return {
12833
+ ok: false,
12834
+ error: `agent[${agentName}].tier_overrides 必须是 object(不是 array / null / 其他类型)`
12835
+ };
12836
+ }
12837
+ const o = raw;
12838
+ const result = {};
12839
+ for (const [key, value] of Object.entries(o)) {
12840
+ if (!isValidTierLevel(key)) {
12841
+ return {
12842
+ ok: false,
12843
+ error: `agent[${agentName}].tier_overrides.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
12844
+ };
12845
+ }
12846
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
12847
+ return {
12848
+ ok: false,
12849
+ error: `agent[${agentName}].tier_overrides.${key}: 必须是 object`
12850
+ };
12851
+ }
12852
+ const ov = value;
12853
+ const partial = {};
12854
+ if (ov.level !== undefined) {
12855
+ if (ov.level !== key) {
12856
+ return {
12857
+ ok: false,
12858
+ error: `agent[${agentName}].tier_overrides.${key}.level="${String(ov.level)}" 与 key "${key}" 错配`
12859
+ };
12860
+ }
12861
+ partial.level = key;
12862
+ }
12863
+ if (ov.model !== undefined) {
12864
+ if (typeof ov.model !== "string" || !PROVIDER_MODEL_RE.test(ov.model)) {
12865
+ return {
12866
+ ok: false,
12867
+ error: `agent[${agentName}].tier_overrides.${key}.model="${String(ov.model)}" 格式非法 (期望 <provider>/<id>)`
12868
+ };
12869
+ }
12870
+ partial.model = ov.model;
12871
+ }
12872
+ if (ov.thinking !== undefined) {
12873
+ const t = normalizeThinking(`agent[${agentName}].tier_overrides.${key}`, ov.thinking);
12874
+ if (!t.ok)
12875
+ return { ok: false, error: t.error };
12876
+ partial.thinking = t.value;
12877
+ }
12878
+ if (ov.fallback_models !== undefined) {
12879
+ const f = normalizeFallbackList(`agent[${agentName}].tier_overrides.${key}.fallback_models`, ov.fallback_models);
12880
+ if (!f.ok)
12881
+ return { ok: false, error: f.error };
12882
+ partial.fallback_models = f.value;
12883
+ }
12884
+ result[key] = partial;
12885
+ }
12886
+ return { ok: true, value: result };
12887
+ }
12888
+ function normalizeTiers(raw) {
12889
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12890
+ return { ok: false, error: "models.tiers 必须是 object(不是 array / null)" };
12891
+ }
12892
+ const o = raw;
12893
+ const cfg = {};
12894
+ if (o.category_map !== undefined) {
12895
+ if (!o.category_map || typeof o.category_map !== "object" || Array.isArray(o.category_map)) {
12896
+ return { ok: false, error: "models.tiers.category_map 必须是 object" };
12897
+ }
12898
+ const map = {};
12899
+ for (const [key, value] of Object.entries(o.category_map)) {
12900
+ if (!isValidTierLevel(key)) {
12901
+ return {
12902
+ ok: false,
12903
+ error: `models.tiers.category_map.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
12904
+ };
12905
+ }
12906
+ if (typeof value !== "string" || value.length === 0) {
12907
+ return {
12908
+ ok: false,
12909
+ error: `models.tiers.category_map.${key}: 必须是非空字符串(category 名)`
12910
+ };
12911
+ }
12912
+ map[key] = value;
12913
+ }
12914
+ cfg.category_map = map;
12915
+ }
12916
+ if (typeof o._doc === "string")
12917
+ cfg._doc = o._doc;
12918
+ return { ok: true, cfg };
12919
+ }
12456
12920
  function resolveAgentModel(config, agent) {
12457
12921
  const a = config.agents[agent];
12458
12922
  if (!a)
@@ -15002,25 +15466,17 @@ function extractEndedSessionID(event) {
15002
15466
  }
15003
15467
  return null;
15004
15468
  }
15005
- function extractSubtaskPart(event) {
15006
- if (!event || typeof event !== "object")
15007
- return null;
15008
- const e = event;
15009
- if (e.type !== "message.part.updated")
15010
- return null;
15011
- const part = e.properties?.part;
15012
- if (!part || typeof part !== "object")
15013
- return null;
15014
- const p = part;
15015
- if (p.type !== "subtask")
15469
+ function extractTaskArgs(args) {
15470
+ if (!args || typeof args !== "object")
15016
15471
  return null;
15017
- if (typeof p.sessionID !== "string" || p.sessionID === "")
15472
+ const a = args;
15473
+ const rawDesc = typeof a["description"] === "string" ? a["description"] : null;
15474
+ const rawPrompt = typeof a["prompt"] === "string" ? a["prompt"] : null;
15475
+ const description26 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
15476
+ const subagentType = typeof a["subagent_type"] === "string" && a["subagent_type"] || typeof a["agent"] === "string" && a["agent"] || typeof a["agentType"] === "string" && a["agentType"] || typeof a["agent_type"] === "string" && a["agent_type"] || null;
15477
+ if (!description26 && !subagentType)
15018
15478
  return null;
15019
- if (typeof p.agent !== "string" || p.agent === "")
15020
- return null;
15021
- if (typeof p.description !== "string" || p.description === "")
15022
- return null;
15023
- return { parentID: p.sessionID, agent: p.agent, description: p.description };
15479
+ return { description: description26, subagentType };
15024
15480
  }
15025
15481
  function enqueuePendingTask(parentID, entry, now = Date.now()) {
15026
15482
  const ts = entry.ts ?? now;
@@ -15249,21 +15705,6 @@ var subtaskHeartbeatServer = async (ctx) => {
15249
15705
  return {
15250
15706
  event: async ({ event }) => {
15251
15707
  await safeAsync(PLUGIN_NAME13, "event", async () => {
15252
- const subtask = extractSubtaskPart(event);
15253
- if (subtask) {
15254
- enqueuePendingTask(subtask.parentID, {
15255
- agent: subtask.agent,
15256
- description: subtask.description
15257
- });
15258
- safeWriteLog(PLUGIN_NAME13, {
15259
- hook: "event",
15260
- type: "message.part.updated.subtask",
15261
- parent: subtask.parentID,
15262
- agent: subtask.agent,
15263
- description_len: subtask.description.length
15264
- });
15265
- return;
15266
- }
15267
15708
  const created = extractCreatedChild(event);
15268
15709
  if (created) {
15269
15710
  const pending = dequeuePendingTask(created.parentID);
@@ -15279,7 +15720,8 @@ var subtaskHeartbeatServer = async (ctx) => {
15279
15720
  child: created.childID,
15280
15721
  parent: created.parentID,
15281
15722
  pending_task_matched: pending !== null,
15282
- agent: record.agent
15723
+ agent: record.agent,
15724
+ description_len: record.description?.length ?? 0
15283
15725
  });
15284
15726
  const startToast = buildStartToast(record);
15285
15727
  const sent = await showToast3(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log7);
@@ -15310,13 +15752,38 @@ var subtaskHeartbeatServer = async (ctx) => {
15310
15752
  }
15311
15753
  });
15312
15754
  },
15313
- "tool.execute.before": async (input) => {
15314
- if (inflight3.size === 0)
15755
+ "tool.execute.before": async (input, output) => {
15756
+ const isTaskTool = input?.tool === "task";
15757
+ if (inflight3.size === 0 && !isTaskTool)
15315
15758
  return;
15316
15759
  await safeAsync(PLUGIN_NAME13, "tool.execute.before", async () => {
15317
15760
  if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
15318
15761
  return;
15319
- recordToolBeat(input.sessionID, input.tool);
15762
+ if (isTaskTool) {
15763
+ const args = output?.args ?? null;
15764
+ safeWriteLog(PLUGIN_NAME13, {
15765
+ hook: "tool.execute.before.task",
15766
+ sessionID: input.sessionID,
15767
+ args_keys: args && typeof args === "object" ? Object.keys(args) : null,
15768
+ args_raw: args
15769
+ });
15770
+ const extracted = extractTaskArgs(args);
15771
+ if (extracted) {
15772
+ enqueuePendingTask(input.sessionID, {
15773
+ agent: extracted.subagentType,
15774
+ description: extracted.description
15775
+ });
15776
+ safeWriteLog(PLUGIN_NAME13, {
15777
+ hook: "tool.execute.before.task.enqueued",
15778
+ parent: input.sessionID,
15779
+ agent: extracted.subagentType,
15780
+ description_len: extracted.description?.length ?? 0
15781
+ });
15782
+ }
15783
+ }
15784
+ if (inflight3.has(input.sessionID)) {
15785
+ recordToolBeat(input.sessionID, input.tool);
15786
+ }
15320
15787
  });
15321
15788
  }
15322
15789
  };
@@ -17473,7 +17940,7 @@ var handler20 = toolPolicyServer;
17473
17940
  init_opencode_plugin_helpers();
17474
17941
  import { existsSync as existsSync5 } from "node:fs";
17475
17942
  import { homedir as homedir7 } from "node:os";
17476
- import { join as join15 } from "node:path";
17943
+ import { join as join16 } from "node:path";
17477
17944
 
17478
17945
  // lib/update-checker-impl.ts
17479
17946
  import { createHash as createHash6 } from "node:crypto";
@@ -17485,12 +17952,12 @@ import {
17485
17952
  readFileSync as readFileSync4,
17486
17953
  readdirSync,
17487
17954
  renameSync,
17488
- statSync as statSync2,
17955
+ statSync as statSync3,
17489
17956
  unlinkSync,
17490
17957
  writeFileSync as writeFileSync2
17491
17958
  } from "node:fs";
17492
17959
  import { homedir as homedir6, tmpdir } from "node:os";
17493
- import { dirname as dirname7, join as join14 } from "node:path";
17960
+ import { dirname as dirname7, join as join15 } from "node:path";
17494
17961
  import { fileURLToPath } from "node:url";
17495
17962
  import * as https from "node:https";
17496
17963
  import * as zlib from "node:zlib";
@@ -17498,7 +17965,7 @@ import * as zlib from "node:zlib";
17498
17965
  // lib/version-injected.ts
17499
17966
  function getInjectedVersion() {
17500
17967
  try {
17501
- const v = "0.3.10";
17968
+ const v = "0.3.11";
17502
17969
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
17503
17970
  return v;
17504
17971
  }
@@ -17588,17 +18055,17 @@ function readLocalVersion() {
17588
18055
  try {
17589
18056
  const here = fileURLToPath(import.meta.url);
17590
18057
  const root = dirname7(dirname7(here));
17591
- const pkg = JSON.parse(readFileSync4(join14(root, "package.json"), "utf8"));
18058
+ const pkg = JSON.parse(readFileSync4(join15(root, "package.json"), "utf8"));
17592
18059
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
17593
18060
  } catch {
17594
18061
  return "0.0.0";
17595
18062
  }
17596
18063
  }
17597
18064
  function defaultCacheDir() {
17598
- return process.env["CODEFORGE_CACHE_DIR"] ?? join14(homedir6(), ".cache", "codeforge");
18065
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join15(homedir6(), ".cache", "codeforge");
17599
18066
  }
17600
18067
  function defaultCacheFile() {
17601
- return join14(defaultCacheDir(), "update-check.json");
18068
+ return join15(defaultCacheDir(), "update-check.json");
17602
18069
  }
17603
18070
  function readCache(file) {
17604
18071
  try {
@@ -17754,14 +18221,14 @@ function defaultHttpFetcher(url, timeoutMs) {
17754
18221
  });
17755
18222
  }
17756
18223
  async function downloadAndExtractBundle(opts) {
17757
- const tmpRoot = opts.tmpDir ?? mkdtempSync(join14(tmpdir(), "codeforge-update-"));
18224
+ const tmpRoot = opts.tmpDir ?? mkdtempSync(join15(tmpdir(), "codeforge-update-"));
17758
18225
  mkdirSync3(tmpRoot, { recursive: true });
17759
18226
  const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
17760
18227
  const tarballBuf = await fetcher(opts.tarballUrl);
17761
18228
  verifyIntegrity(tarballBuf, opts.expectedIntegrity);
17762
18229
  const tarBuf = zlib.gunzipSync(tarballBuf);
17763
18230
  extractTarToDir(tarBuf, tmpRoot);
17764
- const bundlePath = join14(tmpRoot, "package", "dist", "index.js");
18231
+ const bundlePath = join15(tmpRoot, "package", "dist", "index.js");
17765
18232
  if (!existsSync4(bundlePath)) {
17766
18233
  throw new Error(`bundle_not_found: ${bundlePath}`);
17767
18234
  }
@@ -17801,11 +18268,11 @@ function extractTarToDir(tarBuf, destRoot) {
17801
18268
  offset += 512;
17802
18269
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
17803
18270
  const fileBuf = tarBuf.subarray(offset, offset + size);
17804
- const dest = join14(destRoot, fullName);
18271
+ const dest = join15(destRoot, fullName);
17805
18272
  mkdirSync3(dirname7(dest), { recursive: true });
17806
18273
  writeFileSync2(dest, fileBuf);
17807
18274
  } else if (typeFlag === "5") {
17808
- mkdirSync3(join14(destRoot, fullName), { recursive: true });
18275
+ mkdirSync3(join15(destRoot, fullName), { recursive: true });
17809
18276
  }
17810
18277
  offset += Math.ceil(size / 512) * 512;
17811
18278
  }
@@ -17914,10 +18381,10 @@ function cleanupOldBackups(target, keep) {
17914
18381
  const base = target.substring(dir.length + 1);
17915
18382
  const prefix = `${base}.bak.`;
17916
18383
  const all = readdirSync(dir).filter((f) => f.startsWith(prefix)).map((f) => {
17917
- const full = join14(dir, f);
18384
+ const full = join15(dir, f);
17918
18385
  let mtimeMs = 0;
17919
18386
  try {
17920
- mtimeMs = statSync2(full).mtimeMs;
18387
+ mtimeMs = statSync3(full).mtimeMs;
17921
18388
  } catch {}
17922
18389
  return { full, mtimeMs };
17923
18390
  }).sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -17936,7 +18403,7 @@ function loadCompatibility(opts) {
17936
18403
  const root = opts?.cwd ?? inferPluginRoot();
17937
18404
  if (!root)
17938
18405
  return null;
17939
- file = join14(root, "compatibility.json");
18406
+ file = join15(root, "compatibility.json");
17940
18407
  }
17941
18408
  if (!existsSync4(file))
17942
18409
  return null;
@@ -18156,14 +18623,14 @@ function detectOpencodeVersion() {
18156
18623
  }
18157
18624
  function getOpencodeBundlePath() {
18158
18625
  const candidates = [];
18159
- candidates.push(join15(homedir7(), ".config", "opencode", "codeforge", "index.js"));
18626
+ candidates.push(join16(homedir7(), ".config", "opencode", "codeforge", "index.js"));
18160
18627
  if (process.platform === "win32") {
18161
18628
  const appData = process.env["APPDATA"];
18162
18629
  if (appData)
18163
- candidates.push(join15(appData, "opencode", "codeforge", "index.js"));
18630
+ candidates.push(join16(appData, "opencode", "codeforge", "index.js"));
18164
18631
  const localAppData = process.env["LOCALAPPDATA"];
18165
18632
  if (localAppData)
18166
- candidates.push(join15(localAppData, "opencode", "codeforge", "index.js"));
18633
+ candidates.push(join16(localAppData, "opencode", "codeforge", "index.js"));
18167
18634
  }
18168
18635
  for (const c of candidates) {
18169
18636
  if (existsSync5(c))