openclaw-openagent 1.0.13 → 1.0.14

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 (128) hide show
  1. package/dist/src/messaging/executor.d.ts +2 -3
  2. package/dist/src/messaging/executor.js +4 -31
  3. package/dist/src/plugin-ui/assets/openagent-override.js +4 -5
  4. package/dist/src/runtime/update-checker.d.ts +1 -1
  5. package/dist/src/runtime/update-checker.js +9 -46
  6. package/package.json +1 -3
  7. package/src/app/channel-tools.ts +0 -249
  8. package/src/app/discovery-tools.ts +0 -273
  9. package/src/app/download-file-tool.ts +0 -133
  10. package/src/app/hooks.ts +0 -144
  11. package/src/app/index.ts +0 -84
  12. package/src/app/messaging-tools.ts +0 -79
  13. package/src/app/ops-tools.ts +0 -155
  14. package/src/app/remote-agent-tool.ts +0 -762
  15. package/src/app/types.ts +0 -68
  16. package/src/app/upload-file-tool.ts +0 -411
  17. package/src/app/verbose-preflight.ts +0 -190
  18. package/src/auth/config.ts +0 -266
  19. package/src/auth/credential-manager.ts +0 -146
  20. package/src/auth/index.ts +0 -24
  21. package/src/auth/verify.ts +0 -99
  22. package/src/channel.ts +0 -566
  23. package/src/compat.ts +0 -94
  24. package/src/config/config-schema.ts +0 -45
  25. package/src/messaging/aggregator.ts +0 -120
  26. package/src/messaging/collector.ts +0 -89
  27. package/src/messaging/executor.ts +0 -72
  28. package/src/messaging/inbound.ts +0 -150
  29. package/src/messaging/index.ts +0 -11
  30. package/src/messaging/mention-protocol.ts +0 -94
  31. package/src/messaging/process-c2c-request.ts +0 -564
  32. package/src/messaging/process-message.ts +0 -373
  33. package/src/messaging/scheduler.ts +0 -55
  34. package/src/messaging/types.ts +0 -38
  35. package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
  36. package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
  37. package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
  38. package/src/plugin-ui/adapters/oc-unknown.js +0 -48
  39. package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
  40. package/src/plugin-ui/assets/bg.png +0 -0
  41. package/src/plugin-ui/assets/icon.png +0 -0
  42. package/src/plugin-ui/assets/magic.svg +0 -5
  43. package/src/plugin-ui/assets/openagent-override.js +0 -11263
  44. package/src/plugin-ui/build.cjs +0 -463
  45. package/src/plugin-ui/index.ts +0 -18
  46. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
  47. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
  48. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
  49. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
  50. package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
  51. package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -832
  52. package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
  53. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
  54. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
  55. package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
  56. package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
  57. package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
  58. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -262
  59. package/src/plugin-ui/modules/loader/bootstrap.js +0 -133
  60. package/src/plugin-ui/modules/loader/shared-state.js +0 -838
  61. package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
  62. package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
  63. package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
  64. package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
  65. package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
  66. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
  67. package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
  68. package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
  69. package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
  70. package/src/plugin-ui/modules/remote-agent/styles.js +0 -903
  71. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
  72. package/src/plugin-ui/postinstall-deploy.cjs +0 -52
  73. package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
  74. package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
  75. package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
  76. package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
  77. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
  78. package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
  79. package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
  80. package/src/proxy/auth-proxy.ts +0 -414
  81. package/src/runtime/account.ts +0 -549
  82. package/src/runtime/index.ts +0 -7
  83. package/src/runtime/plugin-runtime.ts +0 -94
  84. package/src/runtime/registry.ts +0 -71
  85. package/src/runtime/update-checker.ts +0 -286
  86. package/src/sdk/CLASS_MAP.md +0 -143
  87. package/src/sdk/index.d.ts +0 -126
  88. package/src/sdk/index.js +0 -23990
  89. package/src/sdk/modules/cloud-search-module.js +0 -1117
  90. package/src/sdk/modules/follow-module.js +0 -1069
  91. package/src/sdk/modules/group-module.js +0 -7397
  92. package/src/sdk/modules/relationship-module.js +0 -2269
  93. package/src/sdk/modules/signaling-module.js +0 -1468
  94. package/src/sdk/modules/tim-upload-plugin.js +0 -730
  95. package/src/sdk/node-env/http-request.js +0 -90
  96. package/src/sdk/node-env/index.js +0 -57
  97. package/src/sdk/node-env/storage.js +0 -114
  98. package/src/sdk/package.json +0 -10
  99. package/src/sdk/tsconfig.json +0 -16
  100. package/src/state/pending-invocation-store.ts +0 -43
  101. package/src/state/store.ts +0 -756
  102. package/src/tim/c2c.ts +0 -451
  103. package/src/tim/channels.ts +0 -364
  104. package/src/tim/client.ts +0 -330
  105. package/src/tim/index.ts +0 -18
  106. package/src/tim/messages.ts +0 -166
  107. package/src/tim/sdk-logger-init.ts +0 -50
  108. package/src/tools.ts +0 -10
  109. package/src/transport/factory.ts +0 -95
  110. package/src/transport/oasn/index.ts +0 -17
  111. package/src/transport/oasn/oasn-agent-card.ts +0 -111
  112. package/src/transport/oasn/oasn-discovery.ts +0 -108
  113. package/src/transport/oasn/oasn-files.ts +0 -210
  114. package/src/transport/oasn/oasn-http.ts +0 -529
  115. package/src/transport/oasn/oasn-invocation.ts +0 -604
  116. package/src/transport/oasn/oasn-normalize.ts +0 -133
  117. package/src/transport/oasn/oasn-register.ts +0 -33
  118. package/src/transport/oasn/oasn-transport.ts +0 -318
  119. package/src/transport/oasn/oasn-types.ts +0 -404
  120. package/src/transport/tim/index.ts +0 -8
  121. package/src/transport/tim/tim-transport.ts +0 -515
  122. package/src/transport/types.ts +0 -538
  123. package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
  124. package/src/types/openclaw.d.ts +0 -97
  125. package/src/types/tencentcloud-chat.d.ts +0 -15
  126. package/src/util/http.ts +0 -113
  127. package/src/util/logger.ts +0 -131
  128. package/src/util/url-resolver.ts +0 -77
@@ -1,130 +0,0 @@
1
- // ── OpenAgent Tool Card Model — ToolCard → OpenAgentCardModel 转换 ──
2
- //
3
- // 输入:
4
- // 旧版 OpenClaw ToolCard = { kind, name, args, text }
5
- // 4.11+ OpenClaw ToolCard = { name, args, inputText, outputText }
6
- // 输出: OpenAgentCardModel = { agentId, agentName, taskText, status, rawText, chunks, isError }
7
-
8
- function _clInferToolCardKind(card) {
9
- if (!card) return '';
10
- if (card.kind === 'call' || card.kind === 'result') return card.kind;
11
-
12
- var outputText = card.outputText || '';
13
- var text = card.text || '';
14
- if (String(outputText || text || '').trim()) return 'result';
15
- return 'call';
16
- }
17
-
18
- function _clPickAgentDescription(source) {
19
- if (!source) return '';
20
- var keys = [
21
- 'agent_bio',
22
- 'agentBio',
23
- 'bio',
24
- 'description',
25
- 'desc',
26
- 'agent_description',
27
- 'agentDescription',
28
- 'MOM',
29
- 'mom',
30
- 'introduction',
31
- 'summary'
32
- ];
33
- for (var i = 0; i < keys.length; i++) {
34
- var value = source[keys[i]];
35
- if (typeof value === 'string' && value.trim()) return value.trim();
36
- }
37
- return '';
38
- }
39
-
40
- function _clIsFailureStatusValue(value) {
41
- var normalized = String(value || '').trim().toLowerCase();
42
- return normalized === 'failed'
43
- || normalized === 'failure'
44
- || normalized === 'error'
45
- || normalized === 'errored'
46
- || normalized === 'rejected'
47
- || normalized === 'cancelled'
48
- || normalized === 'canceled';
49
- }
50
-
51
- function _clLooksLikeRemoteFailureText(text) {
52
- var value = String(text || '').trim();
53
- if (!value || value.length > 300) return false;
54
- return /(^|[,。;\s])(调用失败|执行失败|任务失败|远端服务目前连不上|远端服务.*不可用|两次都返回了调用失败)([,。;\s]|$)/.test(value);
55
- }
56
-
57
- function _clInferToolCardError(card, chunks, rawText) {
58
- if (chunks && chunks.some(function(c) { return c && c.type === 'error'; })) return true;
59
- if (!card) return _clLooksLikeRemoteFailureText(rawText);
60
- if (card.isError === true || card.error === true) return true;
61
- if (_clIsFailureStatusValue(card.status)
62
- || _clIsFailureStatusValue(card.state)
63
- || _clIsFailureStatusValue(card.resultStatus)) {
64
- return true;
65
- }
66
- if (card.error && typeof card.error === 'object') return true;
67
- return _clLooksLikeRemoteFailureText(rawText);
68
- }
69
-
70
- function _clApplyAgentIdentityToModel(model) {
71
- if (!model || typeof CL === 'undefined' || !CL || typeof CL.getAgentIdentity !== 'function') return model;
72
- var identity = CL.getAgentIdentity(model.agentId, model.agentName);
73
- if (!identity) return model;
74
- model.agentId = model.agentId || identity.id || identity.agent_id;
75
- if (!model.agentName || model.agentName === 'Remote Agent') {
76
- model.agentName = identity.name || model.agentName;
77
- }
78
- model.agentAvatar = model.agentAvatar || identity.avatar || identity.avatar_url || identity.avatarUrl || null;
79
- if (!model.agentBio) {
80
- model.agentBio = _clPickAgentDescription(identity);
81
- }
82
- return model;
83
- }
84
-
85
- function _clParseToolCardModel(card) {
86
- var inferredKind = _clInferToolCardKind(card);
87
-
88
- // 从 args 提取 agent 信息
89
- var args = (typeof card.args === 'object' && card.args) || {};
90
- var agentId = args.agent_id || args.agentId || null;
91
- var agentName = args.agent_name || args.agentName || agentId || 'Remote Agent';
92
- var agentAvatar = args.agent_avatar || args.agentAvatar || args.avatar || args.avatar_url || args.avatarUrl || null;
93
- var agentBio = _clPickAgentDescription(args);
94
- var agentStars = args.agent_stars || args.agentStars || args.stars || args.claws || '3.2万';
95
- var taskText = args.task || args.message || args.instruction || '';
96
-
97
- // 从 text 解析思考步骤 / 最终文本
98
- var rawText = card.text || card.outputText || '';
99
- var chunks = _clParseChunks(rawText, {
100
- toolCallId: card.toolCallId || '',
101
- rawText: rawText
102
- });
103
- var hasOutput = inferredKind === 'result' && rawText.trim().length > 0;
104
- var isError = _clInferToolCardError(card, chunks, rawText);
105
-
106
- var model = {
107
- agentId: agentId,
108
- agentName: agentName,
109
- agentAvatar: agentAvatar,
110
- agentBio: agentBio,
111
- agentStars: agentStars,
112
- taskText: taskText,
113
- toolCallId: card.toolCallId || '',
114
- status: isError ? 'failed' : (hasOutput ? 'completed' : 'executing'),
115
- rawText: rawText,
116
- text: rawText,
117
- outputText: rawText,
118
- chunks: chunks,
119
- isError: isError,
120
- inferredKind: inferredKind,
121
- };
122
- _clApplyAgentIdentityToModel(model);
123
-
124
- console.log('[cl-model] parsed:', 'agent=' + model.agentName,
125
- 'status=' + model.status,
126
- 'chunks=' + chunks.length,
127
- 'error=' + isError);
128
-
129
- return model;
130
- }
@@ -1,52 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Best-effort deploy for npm install/upgrade.
4
- *
5
- * The browser cache strategy depends on Control UI index.html carrying the
6
- * latest openagent-override.js?v=<hash>. Running the override builder here
7
- * updates reachable OpenClaw Control UI directories immediately after the npm
8
- * package changes, without making npm install fail if OpenClaw is not present.
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const { spawnSync } = require('child_process');
14
-
15
- function log(message) {
16
- console.log('[openagent postinstall] ' + message);
17
- }
18
-
19
- function main() {
20
- if (process.env.OPENAGENT_SKIP_POSTINSTALL_DEPLOY === '1') {
21
- log('skipped by OPENAGENT_SKIP_POSTINSTALL_DEPLOY=1');
22
- return;
23
- }
24
-
25
- const buildScript = path.join(__dirname, 'build.cjs');
26
- if (!fs.existsSync(buildScript)) {
27
- log('build script not found, skipped');
28
- return;
29
- }
30
-
31
- const result = spawnSync(process.execPath, [buildScript], {
32
- cwd: path.join(__dirname, '..', '..'),
33
- env: {
34
- ...process.env,
35
- OPENAGENT_POSTINSTALL_DEPLOY: '1',
36
- },
37
- stdio: 'inherit',
38
- });
39
-
40
- if (result.error) {
41
- log('deploy skipped: ' + result.error.message);
42
- return;
43
- }
44
- if (result.status !== 0) {
45
- log('deploy exited with code ' + result.status + ' (ignored)');
46
- return;
47
- }
48
-
49
- log('deployed latest Control UI override where reachable');
50
- }
51
-
52
- main();
@@ -1,92 +0,0 @@
1
- /**
2
- * Backup — 文件备份与还原
3
- *
4
- * 代码级扩展修改宿主 minified bundle 前,先备份原始文件。
5
- * 卸载时从备份恢复。OC 升级时备份随 control-ui/ 一起被清除。
6
- */
7
-
8
- import { copyFileSync, unlinkSync, existsSync, readFileSync } from 'node:fs';
9
- import { logger } from '../../util/logger.js';
10
-
11
- const BACKUP_SUFFIX = '.openagent-bak';
12
-
13
- /** Patterns that indicate openagent modifications in the bundle.
14
- * NOTE: __openagentMarked is NOT listed here — it's handled by the
15
- * removed-extensions scanner for marked-expose cleanup. */
16
- const OPENAGENT_FINGERPRINTS = [
17
- '__openagentRenderToolCard',
18
- '__openagentOwnsToolCards',
19
- '__tcid',
20
- '__openagentIsRemoteToolName',
21
- '__openagentToolCallIds',
22
- '__openagentKeepToolPayload',
23
- ];
24
- const OPENAGENT_MARKER_PATTERN = '/*openagent:';
25
-
26
- /**
27
- * 检测文件是否包含无 marker 的 openagent 手工补丁。
28
- * 如果文件包含 openagent 特征字符串但没有对应的 marker 注释,
29
- * 说明这是一个被手工修改过的文件,不应该作为"原始备份"保存。
30
- */
31
- function isDirtyBundle(filepath: string): boolean {
32
- try {
33
- const content = readFileSync(filepath, 'utf-8');
34
- const hasFingerprint = OPENAGENT_FINGERPRINTS.some(fp => content.includes(fp));
35
- const hasMarker = content.includes(OPENAGENT_MARKER_PATTERN);
36
- // 有 openagent 特征但没有 marker → 手工补丁
37
- return hasFingerprint && !hasMarker;
38
- } catch {
39
- return false;
40
- }
41
- }
42
-
43
- /**
44
- * 备份文件。如果备份已存在则跳过(保持最早的原始版本)。
45
- * 如果文件包含无 marker 的手工补丁,拒绝备份。
46
- *
47
- * @returns true = 备份就绪(已存在或新创建),false = 检测到脏 bundle,调用方必须中止 apply。
48
- */
49
- export function backupFile(filepath: string): boolean {
50
- const backupPath = filepath + BACKUP_SUFFIX;
51
- if (existsSync(backupPath)) {
52
- logger.debug(`[ui-ext] Backup already exists: ${backupPath}`);
53
- return true;
54
- }
55
-
56
- // 检测脏 bundle:包含 openagent 代码但没有 marker
57
- if (isDirtyBundle(filepath)) {
58
- logger.warn(
59
- `[ui-ext] Dirty bundle detected: ${filepath} contains unmarked openagent patches. ` +
60
- `Refusing to backup or patch. Please reinstall OpenClaw to get a clean bundle, then restart.`
61
- );
62
- return false;
63
- }
64
-
65
- copyFileSync(filepath, backupPath);
66
- logger.debug(`[ui-ext] Backed up: ${filepath}`);
67
- return true;
68
- }
69
-
70
- /**
71
- * 从备份还原文件。
72
- * 还原后删除备份文件。
73
- * 如果备份不存在,记录警告但不抛异常。
74
- */
75
- export function restoreFile(filepath: string): boolean {
76
- const backupPath = filepath + BACKUP_SUFFIX;
77
- if (!existsSync(backupPath)) {
78
- logger.debug(`[ui-ext] No backup found: ${backupPath}`);
79
- return false;
80
- }
81
- copyFileSync(backupPath, filepath);
82
- unlinkSync(backupPath);
83
- logger.debug(`[ui-ext] Restored from backup: ${filepath}`);
84
- return true;
85
- }
86
-
87
- /**
88
- * 检查备份是否存在。
89
- */
90
- export function hasBackup(filepath: string): boolean {
91
- return existsSync(filepath + BACKUP_SUFFIX);
92
- }
@@ -1,276 +0,0 @@
1
- /**
2
- * UIExtensionLoader — 主流程
3
- *
4
- * Manifest Reconciler:对比注册表(期望状态)与清单文件(当前状态),
5
- * 自动完成扩展点的 apply / revert / skip。
6
- *
7
- * 共享文件的扩展按组处理:任何一个需要变更 → 恢复 backup → 重新 apply 全组。
8
- *
9
- * 每次 Gateway 启动时调用 loadUIExtensions()。
10
- */
11
-
12
- import { join } from 'node:path';
13
- import { logger } from '../../util/logger.js';
14
- import { resolveControlUiDir, findFileByPrefix, extractBundleHash } from './locator.js';
15
- import { readManifest, writeManifest, removeManifest } from './manifest.js';
16
- import { restoreFile } from './backup.js';
17
- import { UI_EXTENSIONS } from './registry-regex.js';
18
- import { REMOVED_UI_EXTENSIONS } from './removed-extensions.js';
19
- import type { ManifestData } from './manifest.js';
20
- import type { UIExtension, UIExtensionContext } from './types.js';
21
- import type { RemovedUIExtension, RemovedRevertResult } from './removed-extensions.js';
22
-
23
- // ── 注入策略:正则模式链(无需 .map,21 版本全仿真验证通过)──────────────────
24
-
25
- /**
26
- * 获取当前 bundle 的 hash(用于检测 OC 升级)。
27
- */
28
- function getCurrentBundleHash(controlUiDir: string): string {
29
- const assetsDir = join(controlUiDir, 'assets');
30
- const bundlePath = findFileByPrefix(assetsDir, 'index-');
31
- return bundlePath ? extractBundleHash(bundlePath) : 'unknown';
32
- }
33
-
34
- /**
35
- * 判断一个扩展是否需要变更(新增、更新、或删除)。
36
- */
37
- function needsChange(
38
- ext: UIExtension | null, // null = 已从注册表删除
39
- oldVersion: number | null // null = manifest 中不存在
40
- ): boolean {
41
- if (ext === null) return true; // 需要删除
42
- if (oldVersion === null) return true; // 需要新增
43
- return ext.version !== oldVersion; // 需要更新
44
- }
45
-
46
- /**
47
- * 加载全部 UI 扩展。
48
- *
49
- * Reconciliation 流程:
50
- * 1. 读取 manifest(不存在视为空)
51
- * 2. 检测 bundleHash:不同 → OC 升级 → 全部重新 apply
52
- * 3. 独立扩展逐个 apply/revert
53
- * 4. 共享文件扩展按组处理:任何一个变更 → 恢复 backup → 重新 apply 全组
54
- * 5. 写入新 manifest
55
- */
56
- export async function loadUIExtensions(context: UIExtensionContext = {}): Promise<void> {
57
- const controlUiDir = resolveControlUiDir();
58
-
59
- if (!controlUiDir) {
60
- logger.warn('[ui-ext] control-ui directory not found, skipping');
61
- return;
62
- }
63
-
64
- logger.info(`[ui-ext] control-ui located: ${controlUiDir}`);
65
-
66
- const currentHash = getCurrentBundleHash(controlUiDir);
67
- const manifest = readManifest(controlUiDir);
68
- const removedIds = new Set(REMOVED_UI_EXTENSIONS.map(ext => ext.id));
69
-
70
- // bundleHash 不同 → OC 升级了
71
- const isOcUpgrade = manifest !== null && manifest.bundleHash !== currentHash;
72
- const needFullReapply = isOcUpgrade;
73
- if (isOcUpgrade) {
74
- logger.info(`[ui-ext] OC upgrade detected (${manifest!.bundleHash} → ${currentHash}), re-applying all`);
75
- }
76
-
77
- // These are removed backend dist patches from the previous SSE strategy.
78
- // Scan every startup regardless of manifest state: migration sites may have
79
- // missing manifests, stale backups, or hand-applied copies.
80
- // Track which shared files were restored so we force those groups to rebuild.
81
- const forceSharedRebuild = new Set<string>();
82
-
83
- for (const ext of REMOVED_UI_EXTENSIONS) {
84
- const result = safeRevertRemoved(ext, controlUiDir);
85
- if (result === 'failed') {
86
- logger.error(`[ui-ext] ${ext.id}: removed backend patch could not be reverted; aborting UI extension load`);
87
- return;
88
- }
89
- if (result === 'restored' && ext.sharedFile) {
90
- forceSharedRebuild.add(ext.sharedFile);
91
- logger.info(`[ui-ext] ${ext.id}: shared file '${ext.sharedFile}' restored, will force group rebuild`);
92
- }
93
- }
94
-
95
- const oldExtensions = (manifest && !needFullReapply)
96
- ? Object.fromEntries(Object.entries(manifest.extensions).filter(([id]) => !removedIds.has(id)))
97
- : {};
98
-
99
- // ── 分组:独立扩展 vs 共享文件扩展 ──
100
-
101
- const independentExts: UIExtension[] = [];
102
- const sharedGroups = new Map<string, UIExtension[]>();
103
-
104
- for (const ext of UI_EXTENSIONS) {
105
- if (ext.sharedFile) {
106
- const group = sharedGroups.get(ext.sharedFile) ?? [];
107
- group.push(ext);
108
- sharedGroups.set(ext.sharedFile, group);
109
- } else {
110
- independentExts.push(ext);
111
- }
112
- }
113
-
114
- const newExtensions: Record<string, { version: number }> = {};
115
- let applied = 0;
116
- let skipped = 0;
117
-
118
- // ── 处理独立扩展(逐个 apply/revert)──
119
-
120
- for (const ext of independentExts) {
121
- const old = oldExtensions[ext.id];
122
-
123
- if (old && old.version === ext.version && !ext.alwaysApply) {
124
- skipped++;
125
- newExtensions[ext.id] = { version: ext.version };
126
- logger.debug(`[ui-ext] ${ext.id}: skip (v${ext.version} unchanged)`);
127
- continue;
128
- }
129
-
130
- if (old && old.version !== ext.version) {
131
- // 版本变了,先 revert
132
- logger.debug(`[ui-ext] ${ext.id}: revert (v${old.version} → v${ext.version})`);
133
- safeRevert(ext, controlUiDir);
134
- }
135
-
136
- logger.debug(`[ui-ext] ${ext.id}: apply v${ext.version}`);
137
- if (safeApply(ext, controlUiDir, context)) {
138
- newExtensions[ext.id] = { version: ext.version };
139
- applied++;
140
- }
141
- }
142
-
143
- // ── 处理共享文件扩展(按组批量 reconcile)──
144
-
145
- for (const [sharedFile, group] of sharedGroups) {
146
- // 检查这个组里是否有任何扩展需要变更
147
- let groupNeedsChange = forceSharedRebuild.has(sharedFile);
148
- if (groupNeedsChange) {
149
- logger.info(`[ui-ext] Shared group '${sharedFile}': forced rebuild due to removed extension cleanup`);
150
- }
151
-
152
- if (!groupNeedsChange) {
153
- for (const ext of group) {
154
- const old = oldExtensions[ext.id];
155
- if (needsChange(ext, old?.version ?? null)) {
156
- groupNeedsChange = true;
157
- break;
158
- }
159
- }
160
- }
161
-
162
- // 检查是否有被删除的扩展(在 manifest 中属于该组但不在当前注册表中)
163
- // 如果有,必须触发组重建以清除残留补丁
164
- const groupRegistryIds = new Set(group.map(e => e.id));
165
- for (const oldId of Object.keys(oldExtensions)) {
166
- // 只关心曾经属于本组的扩展(不在当前注册表中 + 不在当前组中)
167
- if (!groupRegistryIds.has(oldId) && !UI_EXTENSIONS.some(e => e.id === oldId)) {
168
- // 无法确定旧扩展属于哪个组(manifest 不记录 sharedFile),保守触发重建
169
- logger.info(`[ui-ext] Orphaned extension '${oldId}' found, triggering group '${sharedFile}' rebuild`);
170
- groupNeedsChange = true;
171
- break;
172
- }
173
- }
174
-
175
- if (!groupNeedsChange) {
176
- // 全组无变更,跳过
177
- for (const ext of group) {
178
- newExtensions[ext.id] = { version: ext.version };
179
- skipped++;
180
- logger.debug(`[ui-ext] ${ext.id}: skip (shared group '${sharedFile}' unchanged)`);
181
- }
182
- continue;
183
- }
184
-
185
- // 有变更 → 恢复 backup → 重新 apply 全组
186
- logger.info(`[ui-ext] Shared group '${sharedFile}': changes detected, rebuilding`);
187
-
188
- // 恢复 backup(仅当之前有补丁时才需要还原)
189
- const assetsDir = join(controlUiDir, 'assets');
190
- const bundlePath = findFileByPrefix(assetsDir, 'index-');
191
- if (bundlePath) {
192
- const restored = restoreFile(bundlePath);
193
- if (restored) {
194
- logger.debug(`[ui-ext] Restored backup for shared group '${sharedFile}'`);
195
- } else {
196
- logger.debug(`[ui-ext] No backup to restore (first install or OC upgrade)`);
197
- }
198
- }
199
-
200
- // 重新 apply 全组中所有应保留的扩展
201
- for (const ext of group) {
202
- logger.debug(`[ui-ext] ${ext.id}: apply v${ext.version} (group rebuild)`);
203
- if (safeApply(ext, controlUiDir, context)) {
204
- newExtensions[ext.id] = { version: ext.version };
205
- applied++;
206
- }
207
- }
208
- }
209
-
210
- // ── 写入新 manifest ──
211
-
212
- const newManifest: ManifestData = {
213
- bundleHash: currentHash,
214
- strategy: 'regex',
215
- extensions: newExtensions,
216
- };
217
- writeManifest(controlUiDir, newManifest);
218
-
219
- logger.info(`[ui-ext] Done: ${applied} applied, ${skipped} skipped`);
220
- }
221
-
222
- /**
223
- * 卸载全部 UI 扩展(插件卸载时调用)。
224
- *
225
- * 共享文件扩展:恢复 backup 即可(一次还原覆盖所有修改)。
226
- * 独立扩展:逐个 revert。
227
- */
228
- export async function unloadUIExtensions(): Promise<void> {
229
- const controlUiDir = resolveControlUiDir();
230
- if (!controlUiDir) return;
231
-
232
- // 先恢复共享文件的 backup
233
- const assetsDir = join(controlUiDir, 'assets');
234
- const bundlePath = findFileByPrefix(assetsDir, 'index-');
235
- if (bundlePath) {
236
- restoreFile(bundlePath);
237
- }
238
-
239
- // 再逆序 revert 独立扩展
240
- for (const ext of UI_EXTENSIONS.slice().reverse()) {
241
- if (!ext.sharedFile) {
242
- safeRevert(ext, controlUiDir);
243
- }
244
- }
245
-
246
- removeManifest(controlUiDir);
247
- logger.info('[ui-ext] All UI extensions unloaded');
248
- }
249
-
250
- // ── Helpers ──────────────────────────────────────────────────────────────
251
-
252
- function safeApply(ext: UIExtension, controlUiDir: string, context: UIExtensionContext): boolean {
253
- try {
254
- return ext.apply(controlUiDir, context);
255
- } catch (err) {
256
- logger.error(`[ui-ext] Failed to apply '${ext.id}': ${(err as Error).message}`);
257
- return false;
258
- }
259
- }
260
-
261
- function safeRevert(ext: UIExtension, controlUiDir: string): void {
262
- try {
263
- ext.revert(controlUiDir);
264
- } catch (err) {
265
- logger.error(`[ui-ext] Failed to revert '${ext.id}': ${(err as Error).message}`);
266
- }
267
- }
268
-
269
- function safeRevertRemoved(ext: RemovedUIExtension, controlUiDir: string): RemovedRevertResult {
270
- try {
271
- return ext.revert(controlUiDir);
272
- } catch (err) {
273
- logger.error(`[ui-ext] Failed to revert removed extension '${ext.id}': ${(err as Error).message}`);
274
- return 'failed';
275
- }
276
- }