codexmate 0.0.44 → 0.0.47

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 (41) hide show
  1. package/README.md +20 -7
  2. package/README.zh.md +20 -7
  3. package/cli.js +473 -3
  4. package/package.json +2 -1
  5. package/web-ui/app.js +42 -5
  6. package/web-ui/index.html +2 -0
  7. package/web-ui/modules/app.computed.index.mjs +3 -1
  8. package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
  9. package/web-ui/modules/app.computed.prompts.mjs +28 -0
  10. package/web-ui/modules/app.computed.session.mjs +23 -1
  11. package/web-ui/modules/app.constants.mjs +13 -0
  12. package/web-ui/modules/app.methods.agents.mjs +50 -4
  13. package/web-ui/modules/app.methods.index.mjs +6 -0
  14. package/web-ui/modules/app.methods.navigation.mjs +2 -1
  15. package/web-ui/modules/app.methods.opencode-config.mjs +228 -0
  16. package/web-ui/modules/app.methods.session-actions.mjs +10 -0
  17. package/web-ui/modules/app.methods.startup-claude.mjs +3 -1
  18. package/web-ui/modules/app.methods.tool-config-permissions.mjs +3 -2
  19. package/web-ui/modules/config-mode.computed.mjs +17 -1
  20. package/web-ui/modules/i18n/locales/en.mjs +66 -5
  21. package/web-ui/modules/i18n/locales/ja.mjs +66 -5
  22. package/web-ui/modules/i18n/locales/vi.mjs +74 -0
  23. package/web-ui/modules/i18n/locales/zh-tw.mjs +1269 -0
  24. package/web-ui/modules/i18n/locales/zh.mjs +66 -5
  25. package/web-ui/modules/i18n.dict.mjs +2 -0
  26. package/web-ui/modules/i18n.mjs +3 -2
  27. package/web-ui/partials/index/layout-footer.html +1 -1
  28. package/web-ui/partials/index/layout-header.html +70 -2
  29. package/web-ui/partials/index/modal-config-template-agents.html +12 -13
  30. package/web-ui/partials/index/panel-config-claude.html +6 -12
  31. package/web-ui/partials/index/panel-config-codex.html +6 -11
  32. package/web-ui/partials/index/panel-config-opencode.html +166 -0
  33. package/web-ui/partials/index/panel-prompts.html +100 -0
  34. package/web-ui/partials/index/panel-sessions.html +30 -10
  35. package/web-ui/partials/index/panel-usage.html +34 -18
  36. package/web-ui/res/web-ui-render.precompiled.js +932 -183
  37. package/web-ui/styles/controls-forms.css +62 -4
  38. package/web-ui/styles/modals-core.css +162 -0
  39. package/web-ui/styles/responsive.css +65 -5
  40. package/web-ui/styles/sessions-toolbar-trash.css +45 -10
  41. package/web-ui/styles/sessions-usage.css +31 -2
package/cli.js CHANGED
@@ -185,6 +185,11 @@ const DEFAULT_WEB_OPEN_HOST = '127.0.0.1';
185
185
  const CONFIG_DIR = path.join(os.homedir(), '.codex');
186
186
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml');
187
187
  const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
188
+ const OPENCODE_CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode');
189
+ const OPENCODE_CONFIG_ENV_FILE = process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : '';
190
+ const OPENCODE_GLOBAL_JSONC_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
191
+ const OPENCODE_GLOBAL_JSON_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
192
+ const OPENCODE_LEGACY_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'config.json');
188
193
  const AUTH_PROFILES_DIR = path.join(CONFIG_DIR, 'auth-profiles');
189
194
  const AUTH_REGISTRY_FILE = path.join(AUTH_PROFILES_DIR, 'registry.json');
190
195
  const MODELS_FILE = path.join(CONFIG_DIR, 'models.json');
@@ -214,6 +219,8 @@ const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
214
219
  const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
215
220
  const CODEXMATE_DIR = path.join(os.homedir(), '.codexmate');
216
221
  const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
222
+ const CODEXMATE_OPENCODE_DIR = path.join(CODEXMATE_DIR, 'opencode');
223
+ const CODEXMATE_OPENCODE_PROVIDER_STORE_FILE = path.join(CODEXMATE_OPENCODE_DIR, 'providers.json');
217
224
  const CODEXMATE_SESSIONS_DIR = path.join(CODEXMATE_DIR, 'sessions');
218
225
  const CODEXMATE_DERIVED_SESSIONS_DIR = path.join(CODEXMATE_SESSIONS_DIR, 'derived');
219
226
  const CODEXMATE_DERIVED_CODEX_DIR = path.join(CODEXMATE_DERIVED_SESSIONS_DIR, 'codex');
@@ -860,8 +867,8 @@ function isPlainObject(value) {
860
867
  return !!value && typeof value === 'object' && !Array.isArray(value);
861
868
  }
862
869
 
863
- const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude']);
864
- const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false });
870
+ const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude', 'opencode']);
871
+ const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false, opencode: false });
865
872
  let toolConfigWriteGuardDepth = 0;
866
873
 
867
874
  function enterToolConfigWriteGuard() {
@@ -887,7 +894,8 @@ function normalizeToolConfigPermissions(value) {
887
894
  const source = isPlainObject(value) ? value : {};
888
895
  return {
889
896
  codex: source.codex === true,
890
- claude: source.claude === true
897
+ claude: source.claude === true,
898
+ opencode: source.opencode === true
891
899
  };
892
900
  }
893
901
 
@@ -967,8 +975,13 @@ function getApiToolConfigWriteTarget(action) {
967
975
  'claude-local-bridge-set-excluded',
968
976
  'claude-local-bridge-sync-providers'
969
977
  ]);
978
+ const opencodeWriteActions = new Set([
979
+ 'apply-opencode-config',
980
+ 'update-opencode-selection'
981
+ ]);
970
982
  if (codexWriteActions.has(name)) return 'codex';
971
983
  if (claudeWriteActions.has(name)) return 'claude';
984
+ if (opencodeWriteActions.has(name)) return 'opencode';
972
985
  return '';
973
986
  }
974
987
 
@@ -9628,6 +9641,454 @@ function applyClaudeSettingsRaw(params = {}) {
9628
9641
  }
9629
9642
  }
9630
9643
 
9644
+ function getOpencodeConfigCandidates() {
9645
+ const candidates = [
9646
+ OPENCODE_CONFIG_ENV_FILE,
9647
+ OPENCODE_GLOBAL_JSONC_CONFIG_FILE,
9648
+ OPENCODE_GLOBAL_JSON_CONFIG_FILE,
9649
+ OPENCODE_LEGACY_CONFIG_FILE
9650
+ ]
9651
+ .filter(Boolean)
9652
+ .map(item => path.resolve(item));
9653
+ return [...new Set(candidates)];
9654
+ }
9655
+
9656
+ function resolveOpencodeConfigFile() {
9657
+ const candidates = getOpencodeConfigCandidates();
9658
+ for (const candidate of candidates) {
9659
+ if (fs.existsSync(candidate)) {
9660
+ return candidate;
9661
+ }
9662
+ }
9663
+ return OPENCODE_CONFIG_ENV_FILE || OPENCODE_GLOBAL_JSONC_CONFIG_FILE;
9664
+ }
9665
+
9666
+ function readOpencodeConfigObject(content) {
9667
+ const raw = typeof content === 'string' ? content : '';
9668
+ if (!raw.trim()) {
9669
+ return {};
9670
+ }
9671
+ const parsed = JSON5.parse(raw);
9672
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
9673
+ throw new Error('OpenCode config must be a JSON/JSONC object');
9674
+ }
9675
+ return parsed;
9676
+ }
9677
+
9678
+ function normalizeOpencodeAgentName(value) {
9679
+ const name = typeof value === 'string' ? value.trim() : '';
9680
+ return /^[a-zA-Z0-9_.-]+$/.test(name) ? name : '';
9681
+ }
9682
+
9683
+ function normalizeOpencodeProviderName(value) {
9684
+ const name = typeof value === 'string' ? value.trim().toLowerCase() : '';
9685
+ return /^[a-z0-9_.-]+$/.test(name) ? name : '';
9686
+ }
9687
+
9688
+ function splitOpencodeModelRef(modelRef) {
9689
+ const raw = typeof modelRef === 'string' ? modelRef.trim() : '';
9690
+ const slash = raw.indexOf('/');
9691
+ if (slash <= 0 || slash === raw.length - 1) {
9692
+ return { provider: '', model: raw };
9693
+ }
9694
+ return {
9695
+ provider: normalizeOpencodeProviderName(raw.slice(0, slash)),
9696
+ model: raw.slice(slash + 1).trim()
9697
+ };
9698
+ }
9699
+
9700
+ function joinOpencodeModelRef(providerName, model) {
9701
+ const provider = normalizeOpencodeProviderName(providerName);
9702
+ const modelName = typeof model === 'string' ? model.trim().replace(/^\/+/, '') : '';
9703
+ return provider && modelName ? `${provider}/${modelName}` : '';
9704
+ }
9705
+
9706
+ function getRecord(value) {
9707
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
9708
+ }
9709
+
9710
+ function stableOpencodeJson(value) {
9711
+ if (Array.isArray(value)) {
9712
+ return `[${value.map(item => stableOpencodeJson(item)).join(',')}]`;
9713
+ }
9714
+ if (value && typeof value === 'object') {
9715
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableOpencodeJson(value[key])}`).join(',')}}`;
9716
+ }
9717
+ return JSON.stringify(value);
9718
+ }
9719
+
9720
+ function hashOpencodeManagedValue(value) {
9721
+ return crypto.createHash('sha256').update(stableOpencodeJson(value === undefined ? null : value)).digest('hex');
9722
+ }
9723
+
9724
+ function normalizeOpencodeProviderStore(raw) {
9725
+ const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
9726
+ const providers = {};
9727
+ const sourceProviders = getRecord(source.providers);
9728
+ for (const [rawName, rawProvider] of Object.entries(sourceProviders)) {
9729
+ const name = normalizeOpencodeProviderName(rawName);
9730
+ const provider = getRecord(rawProvider);
9731
+ if (!name) continue;
9732
+ const apiKey = typeof provider.apiKey === 'string' ? provider.apiKey : '';
9733
+ const model = typeof provider.model === 'string' ? provider.model.trim() : '';
9734
+ const maxTokens = Number.isFinite(Number(provider.maxTokens)) && Number(provider.maxTokens) > 0
9735
+ ? Number(provider.maxTokens)
9736
+ : null;
9737
+ const reasoningEffort = typeof provider.reasoningEffort === 'string' ? provider.reasoningEffort.trim().toLowerCase() : '';
9738
+ providers[name] = {
9739
+ apiKey,
9740
+ model,
9741
+ disabled: provider.disabled === true,
9742
+ maxTokens,
9743
+ reasoningEffort: ['low', 'medium', 'high'].includes(reasoningEffort) ? reasoningEffort : '',
9744
+ updatedAt: typeof provider.updatedAt === 'string' ? provider.updatedAt : ''
9745
+ };
9746
+ }
9747
+ const rawLastApplied = getRecord(source.lastApplied);
9748
+ const lastAppliedProvider = normalizeOpencodeProviderName(rawLastApplied.provider);
9749
+ const lastApplied = lastAppliedProvider
9750
+ ? {
9751
+ provider: lastAppliedProvider,
9752
+ modelRef: typeof rawLastApplied.modelRef === 'string' ? rawLastApplied.modelRef : '',
9753
+ providerHash: typeof rawLastApplied.providerHash === 'string' ? rawLastApplied.providerHash : '',
9754
+ disabledProvidersHash: typeof rawLastApplied.disabledProvidersHash === 'string' ? rawLastApplied.disabledProvidersHash : '',
9755
+ providerCreatedByCodexMate: rawLastApplied.providerCreatedByCodexMate === true,
9756
+ disabledProviderAddedByCodexMate: rawLastApplied.disabledProviderAddedByCodexMate === true
9757
+ }
9758
+ : null;
9759
+ return {
9760
+ version: 1,
9761
+ providers,
9762
+ lastApplied
9763
+ };
9764
+ }
9765
+
9766
+ function readOpencodeProviderStore() {
9767
+ if (!fs.existsSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE)) {
9768
+ return normalizeOpencodeProviderStore({});
9769
+ }
9770
+ try {
9771
+ const raw = JSON.parse(fs.readFileSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, 'utf-8') || '{}');
9772
+ return normalizeOpencodeProviderStore(raw);
9773
+ } catch (e) {
9774
+ return normalizeOpencodeProviderStore({});
9775
+ }
9776
+ }
9777
+
9778
+ function writeOpencodeProviderStore(store) {
9779
+ ensureDir(CODEXMATE_OPENCODE_DIR);
9780
+ writeJsonAtomic(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, normalizeOpencodeProviderStore(store));
9781
+ try {
9782
+ fs.chmodSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, 0o600);
9783
+ } catch (e) {}
9784
+ }
9785
+
9786
+ function summarizeOpencodeConfig(config = {}, targetPath = resolveOpencodeConfigFile(), exists = false, providerStore = readOpencodeProviderStore()) {
9787
+ const providers = getRecord(config.provider);
9788
+ const storedProviders = getRecord(providerStore.providers);
9789
+ const agents = getRecord(config.agent);
9790
+ const disabledProviders = Array.isArray(config.disabled_providers)
9791
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean)
9792
+ : [];
9793
+ const topLevelModel = splitOpencodeModelRef(config.model);
9794
+ const agentEntries = Object.entries(agents)
9795
+ .filter(([, agent]) => agent && typeof agent === 'object' && !Array.isArray(agent))
9796
+ .map(([name, agent]) => {
9797
+ const modelRef = typeof agent.model === 'string' ? agent.model : '';
9798
+ const parsedModel = splitOpencodeModelRef(modelRef);
9799
+ const requestBody = getRecord(getRecord(agent.request).body);
9800
+ return {
9801
+ name,
9802
+ model: parsedModel.model || modelRef,
9803
+ modelRef,
9804
+ provider: parsedModel.provider,
9805
+ maxTokens: Number.isFinite(Number(requestBody.max_tokens)) ? Number(requestBody.max_tokens) : null,
9806
+ reasoningEffort: typeof requestBody.reasoning_effort === 'string' ? requestBody.reasoning_effort : ''
9807
+ };
9808
+ });
9809
+ const preferredAgentName = normalizeOpencodeAgentName(config.default_agent) || 'build';
9810
+ const primaryAgent = agentEntries.find(item => item.name === preferredAgentName)
9811
+ || agentEntries.find(item => item.name === 'build')
9812
+ || agentEntries[0]
9813
+ || null;
9814
+ const currentProvider = topLevelModel.provider || (primaryAgent && primaryAgent.provider) || '';
9815
+ const currentModel = topLevelModel.model || (primaryAgent && primaryAgent.model) || '';
9816
+ const providerNames = [...new Set([
9817
+ ...Object.keys(storedProviders),
9818
+ ...Object.keys(providers),
9819
+ currentProvider,
9820
+ ...(agentEntries.map(item => item.provider))
9821
+ ].map(item => normalizeOpencodeProviderName(item)).filter(Boolean))];
9822
+ return {
9823
+ exists: !!exists,
9824
+ targetPath,
9825
+ providerStorePath: CODEXMATE_OPENCODE_PROVIDER_STORE_FILE,
9826
+ providers: providerNames.map((name) => {
9827
+ const provider = getRecord(providers[name]);
9828
+ const storedProvider = getRecord(storedProviders[name]);
9829
+ const options = getRecord(provider.options);
9830
+ const apiKey = typeof options.apiKey === 'string' && options.apiKey.trim()
9831
+ ? options.apiKey
9832
+ : (typeof storedProvider.apiKey === 'string' ? storedProvider.apiKey : '');
9833
+ return {
9834
+ name,
9835
+ apiKey: maskKey(apiKey),
9836
+ hasKey: apiKey.trim().length > 0,
9837
+ disabled: disabledProviders.includes(name) || storedProvider.disabled === true,
9838
+ source: Object.prototype.hasOwnProperty.call(providers, name) ? 'opencode' : 'codexmate'
9839
+ };
9840
+ }),
9841
+ agents: agentEntries,
9842
+ currentAgent: primaryAgent ? primaryAgent.name : preferredAgentName,
9843
+ currentProvider,
9844
+ currentModel,
9845
+ currentModelRef: joinOpencodeModelRef(currentProvider, currentModel),
9846
+ autoCompact: getRecord(config.compaction).auto !== false,
9847
+ redacted: true
9848
+ };
9849
+ }
9850
+
9851
+ function readOpencodeConfigInfo() {
9852
+ const targetPath = resolveOpencodeConfigFile();
9853
+ if (!fs.existsSync(targetPath)) {
9854
+ const config = { $schema: 'https://opencode.ai/config.json' };
9855
+ return {
9856
+ ...summarizeOpencodeConfig(config, targetPath, false),
9857
+ content: JSON.stringify(config, null, 2) + '\n',
9858
+ candidates: getOpencodeConfigCandidates()
9859
+ };
9860
+ }
9861
+ try {
9862
+ const raw = fs.readFileSync(targetPath, 'utf-8');
9863
+ const config = readOpencodeConfigObject(raw);
9864
+ return {
9865
+ ...summarizeOpencodeConfig(config, targetPath, true),
9866
+ content: raw || '{}',
9867
+ candidates: getOpencodeConfigCandidates()
9868
+ };
9869
+ } catch (e) {
9870
+ return {
9871
+ error: e.message || '读取 OpenCode 配置失败',
9872
+ exists: true,
9873
+ targetPath,
9874
+ candidates: getOpencodeConfigCandidates()
9875
+ };
9876
+ }
9877
+ }
9878
+
9879
+ function applyOpencodeConfigRaw(params = {}) {
9880
+ assertToolConfigWriteAllowed('opencode');
9881
+ const content = typeof params.content === 'string' ? params.content : '';
9882
+ if (!content.trim()) {
9883
+ return { error: '内容不能为空' };
9884
+ }
9885
+ if (content.length > 1024 * 1024) {
9886
+ return { error: '内容过大(最大 1MB)' };
9887
+ }
9888
+ let parsed;
9889
+ try {
9890
+ parsed = readOpencodeConfigObject(content);
9891
+ } catch (e) {
9892
+ return { error: `OpenCode JSON/JSONC 解析失败: ${e.message}` };
9893
+ }
9894
+ const targetPath = resolveOpencodeConfigFile();
9895
+ try {
9896
+ ensureDir(path.dirname(targetPath));
9897
+ backupFileIfNeededOnce(targetPath);
9898
+ fs.writeFileSync(targetPath, JSON.stringify(parsed, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
9899
+ return {
9900
+ success: true,
9901
+ targetPath,
9902
+ content: JSON.stringify(parsed, null, 2) + '\n',
9903
+ ...summarizeOpencodeConfig(parsed, targetPath, true)
9904
+ };
9905
+ } catch (e) {
9906
+ return { error: e.message || '写入 OpenCode 配置失败' };
9907
+ }
9908
+ }
9909
+
9910
+ function removePreviousCodexMateOpencodeProjection(config, lastApplied, nextProviderName) {
9911
+ const previousProvider = normalizeOpencodeProviderName(lastApplied && lastApplied.provider);
9912
+ const nextProvider = normalizeOpencodeProviderName(nextProviderName);
9913
+ if (!previousProvider || previousProvider === nextProvider) return;
9914
+
9915
+ const providers = getRecord(config.provider);
9916
+ if (lastApplied.providerCreatedByCodexMate === true && providers[previousProvider] && lastApplied.providerHash) {
9917
+ const currentHash = hashOpencodeManagedValue(providers[previousProvider]);
9918
+ if (currentHash === lastApplied.providerHash) {
9919
+ delete providers[previousProvider];
9920
+ }
9921
+ }
9922
+ if (Object.keys(providers).length) {
9923
+ config.provider = providers;
9924
+ } else {
9925
+ delete config.provider;
9926
+ }
9927
+
9928
+ if (lastApplied.disabledProviderAddedByCodexMate === true && Array.isArray(config.disabled_providers) && lastApplied.disabledProvidersHash) {
9929
+ const currentDisabled = config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).sort();
9930
+ if (hashOpencodeManagedValue(currentDisabled) === lastApplied.disabledProvidersHash) {
9931
+ const nextDisabled = currentDisabled.filter(item => item !== previousProvider);
9932
+ if (nextDisabled.length) {
9933
+ config.disabled_providers = nextDisabled;
9934
+ } else {
9935
+ delete config.disabled_providers;
9936
+ }
9937
+ }
9938
+ }
9939
+ }
9940
+
9941
+ function updateOpencodeSelection(params = {}) {
9942
+ assertToolConfigWriteAllowed('opencode');
9943
+ const providerName = normalizeOpencodeProviderName(params.provider);
9944
+ const model = typeof params.model === 'string' ? params.model.trim() : '';
9945
+ const agentName = normalizeOpencodeAgentName(params.agent || 'build') || 'build';
9946
+ if (!providerName) {
9947
+ return { error: '请选择 OpenCode provider' };
9948
+ }
9949
+ if (!model) {
9950
+ return { error: '请选择或输入 OpenCode model' };
9951
+ }
9952
+
9953
+ const targetPath = resolveOpencodeConfigFile();
9954
+ let config = {};
9955
+ if (fs.existsSync(targetPath)) {
9956
+ try {
9957
+ config = readOpencodeConfigObject(fs.readFileSync(targetPath, 'utf-8'));
9958
+ } catch (e) {
9959
+ return { error: `OpenCode JSON/JSONC 解析失败: ${e.message}` };
9960
+ }
9961
+ }
9962
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
9963
+ config = {};
9964
+ }
9965
+
9966
+ const providerStore = readOpencodeProviderStore();
9967
+ removePreviousCodexMateOpencodeProjection(config, providerStore.lastApplied, providerName);
9968
+ const providerExistedBeforeApply = !!(getRecord(config.provider)[providerName]);
9969
+ const disabledContainedBeforeApply = Array.isArray(config.disabled_providers)
9970
+ && config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).includes(providerName);
9971
+
9972
+ if (!config.$schema) {
9973
+ config.$schema = 'https://opencode.ai/config.json';
9974
+ }
9975
+ const modelRef = joinOpencodeModelRef(providerName, model);
9976
+ config.model = modelRef;
9977
+
9978
+ if (!config.provider || typeof config.provider !== 'object' || Array.isArray(config.provider)) {
9979
+ config.provider = {};
9980
+ }
9981
+ const storedProvider = getRecord(providerStore.providers[providerName]);
9982
+ const previousProvider = getRecord(config.provider[providerName]);
9983
+ const previousOptions = getRecord(previousProvider.options);
9984
+ const explicitApiKey = typeof params.apiKey === 'string' ? params.apiKey.trim() : '';
9985
+ const storedApiKey = typeof storedProvider.apiKey === 'string' ? storedProvider.apiKey.trim() : '';
9986
+ const apiKey = explicitApiKey || storedApiKey;
9987
+ config.provider[providerName] = {
9988
+ ...previousProvider,
9989
+ options: {
9990
+ ...previousOptions
9991
+ }
9992
+ };
9993
+ if (apiKey) {
9994
+ config.provider[providerName].options.apiKey = apiKey;
9995
+ }
9996
+ if (Object.keys(config.provider[providerName].options).length === 0) {
9997
+ delete config.provider[providerName].options;
9998
+ }
9999
+
10000
+ const disabledSet = new Set(Array.isArray(config.disabled_providers)
10001
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean)
10002
+ : []);
10003
+ if (params.disabled === true) {
10004
+ disabledSet.add(providerName);
10005
+ } else {
10006
+ disabledSet.delete(providerName);
10007
+ }
10008
+ if (disabledSet.size) {
10009
+ config.disabled_providers = [...disabledSet].sort();
10010
+ } else {
10011
+ delete config.disabled_providers;
10012
+ }
10013
+
10014
+ if (!config.agent || typeof config.agent !== 'object' || Array.isArray(config.agent)) {
10015
+ config.agent = {};
10016
+ }
10017
+ const coreAgents = params.applyToCoreAgents === true
10018
+ ? ['build', 'plan', 'general', 'title', 'summary', 'compaction']
10019
+ : [agentName];
10020
+ for (const name of coreAgents) {
10021
+ const previousAgent = getRecord(config.agent[name]);
10022
+ const previousRequest = getRecord(previousAgent.request);
10023
+ const previousBody = getRecord(previousRequest.body);
10024
+ const nextAgent = {
10025
+ ...previousAgent,
10026
+ model: modelRef
10027
+ };
10028
+ const requestBody = { ...previousBody };
10029
+ const maxTokens = normalizePositiveIntegerParam(params.maxTokens);
10030
+ if (maxTokens !== null) {
10031
+ requestBody.max_tokens = maxTokens;
10032
+ }
10033
+ const effort = typeof params.reasoningEffort === 'string' ? params.reasoningEffort.trim().toLowerCase() : '';
10034
+ if (effort === 'low' || effort === 'medium' || effort === 'high') {
10035
+ requestBody.reasoning_effort = effort;
10036
+ }
10037
+ if (Object.keys(requestBody).length) {
10038
+ nextAgent.request = {
10039
+ ...previousRequest,
10040
+ body: requestBody
10041
+ };
10042
+ }
10043
+ config.agent[name] = nextAgent;
10044
+ }
10045
+ if (!config.default_agent) {
10046
+ config.default_agent = agentName;
10047
+ }
10048
+ if (!config.compaction || typeof config.compaction !== 'object' || Array.isArray(config.compaction)) {
10049
+ config.compaction = {};
10050
+ }
10051
+ config.compaction.auto = params.autoCompact !== false;
10052
+
10053
+ const maxTokens = normalizePositiveIntegerParam(params.maxTokens);
10054
+ const effort = typeof params.reasoningEffort === 'string' ? params.reasoningEffort.trim().toLowerCase() : '';
10055
+ providerStore.providers[providerName] = {
10056
+ ...storedProvider,
10057
+ apiKey: apiKey || '',
10058
+ model,
10059
+ disabled: params.disabled === true,
10060
+ maxTokens,
10061
+ reasoningEffort: ['low', 'medium', 'high'].includes(effort) ? effort : '',
10062
+ updatedAt: new Date().toISOString()
10063
+ };
10064
+
10065
+ try {
10066
+ ensureDir(path.dirname(targetPath));
10067
+ backupFileIfNeededOnce(targetPath);
10068
+ const content = JSON.stringify(config, null, 2) + '\n';
10069
+ fs.writeFileSync(targetPath, content, { encoding: 'utf-8', mode: 0o600 });
10070
+ const disabledProviders = Array.isArray(config.disabled_providers)
10071
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).sort()
10072
+ : [];
10073
+ providerStore.lastApplied = {
10074
+ provider: providerName,
10075
+ modelRef,
10076
+ providerHash: hashOpencodeManagedValue(getRecord(config.provider)[providerName]),
10077
+ disabledProvidersHash: hashOpencodeManagedValue(disabledProviders),
10078
+ providerCreatedByCodexMate: providerExistedBeforeApply !== true,
10079
+ disabledProviderAddedByCodexMate: params.disabled === true && disabledContainedBeforeApply !== true
10080
+ };
10081
+ writeOpencodeProviderStore(providerStore);
10082
+ return {
10083
+ success: true,
10084
+ targetPath,
10085
+ ...summarizeOpencodeConfig(config, targetPath, true, providerStore),
10086
+ content
10087
+ };
10088
+ } catch (e) {
10089
+ return { error: e.message || '写入 OpenCode 配置失败' };
10090
+ }
10091
+ }
9631
10092
  // API: 打包 Claude 配置目录(系统 zip 可用则使用,否则回退 zip-lib)
9632
10093
  async function prepareClaudeDirDownload() {
9633
10094
  return await prepareDirectoryDownload(CLAUDE_DIR, {
@@ -11452,6 +11913,15 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11452
11913
  case 'apply-claude-settings-raw':
11453
11914
  result = applyClaudeSettingsRaw(params || {});
11454
11915
  break;
11916
+ case 'get-opencode-config':
11917
+ result = readOpencodeConfigInfo();
11918
+ break;
11919
+ case 'apply-opencode-config':
11920
+ result = applyOpencodeConfigRaw(params || {});
11921
+ break;
11922
+ case 'update-opencode-selection':
11923
+ result = updateOpencodeSelection(params || {});
11924
+ break;
11455
11925
  case 'apply-claude-config':
11456
11926
  result = await applyToClaudeSettings(params.config);
11457
11927
  if (result && !result.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.44",
3
+ "version": "0.0.47",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -72,6 +72,7 @@
72
72
  "license": "Apache-2.0",
73
73
  "devDependencies": {
74
74
  "@vue/compiler-dom": "^3.5.30",
75
+ "opencc-js": "^1.3.1",
75
76
  "vitepress": "^1.6.4"
76
77
  }
77
78
  }
package/web-ui/app.js CHANGED
@@ -73,6 +73,7 @@ document.addEventListener('DOMContentLoaded', () => {
73
73
  showOpenclawConfigModal: false,
74
74
  showConfigTemplateModal: false,
75
75
  showAgentsModal: false,
76
+ promptsSubTab: 'codex',
76
77
  showSkillsModal: false,
77
78
  showHealthCheckModal: false,
78
79
  showCodexBridgePoolModal: false,
@@ -382,14 +383,15 @@ document.addEventListener('DOMContentLoaded', () => {
382
383
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
383
384
  return {
384
385
  codex: parsed.codex === true,
385
- claude: parsed.claude === true
386
+ claude: parsed.claude === true,
387
+ opencode: parsed.opencode === true
386
388
  };
387
389
  }
388
390
  }
389
391
  } catch (_) {}
390
- return { codex: false, claude: false };
392
+ return { codex: false, claude: false, opencode: false };
391
393
  })(),
392
- toolConfigPermissionSaving: { codex: false, claude: false },
394
+ toolConfigPermissionSaving: { codex: false, claude: false, opencode: false },
393
395
  sessionTrashEnabled: true,
394
396
  sessionTrashItems: [],
395
397
  sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
@@ -410,6 +412,28 @@ document.addEventListener('DOMContentLoaded', () => {
410
412
  claudeImportLoading: false,
411
413
  codexImportLoading: false,
412
414
  codexAuthProfiles: [],
415
+ opencodeConfigPath: '',
416
+ opencodeProviderStorePath: '',
417
+ opencodeConfigExists: false,
418
+ opencodeContent: '{}',
419
+ opencodeLoading: false,
420
+ opencodeSaving: false,
421
+ opencodeApplying: false,
422
+ opencodeError: '',
423
+ opencodeImportError: '',
424
+ opencodeImportFileName: '',
425
+ opencodeProviders: [],
426
+ opencodeAgents: [],
427
+ opencodeProvider: 'anthropic',
428
+ opencodeModel: '',
429
+ opencodeApiKey: '',
430
+ opencodeShowKey: false,
431
+ opencodeProviderDisabled: false,
432
+ opencodeAgent: 'build',
433
+ opencodeApplyToCoreAgents: true,
434
+ opencodeAutoCompact: true,
435
+ opencodeMaxTokens: '',
436
+ opencodeReasoningEffort: '',
413
437
  forceCompactLayout: false,
414
438
  taskOrchestrationTabEnabled: true,
415
439
  taskOrchestration: {
@@ -486,7 +510,7 @@ document.addEventListener('DOMContentLoaded', () => {
486
510
  }
487
511
  {
488
512
  const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
489
- const mainTabSet = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash']);
513
+ const mainTabSet = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash', 'prompts']);
490
514
  let restored = null;
491
515
  try {
492
516
  const raw = localStorage.getItem(NAV_STATE_STORAGE_KEY) || '';
@@ -521,7 +545,7 @@ document.addEventListener('DOMContentLoaded', () => {
521
545
  if (nextConfigMode && typeof this.switchConfigMode === 'function') {
522
546
  this.__navStateRestoring = true;
523
547
  try {
524
- if (nextConfigMode === 'codex' || nextConfigMode === 'claude' || nextConfigMode === 'openclaw') {
548
+ if (nextConfigMode === 'codex' || nextConfigMode === 'claude' || nextConfigMode === 'openclaw' || nextConfigMode === 'opencode') {
525
549
  this.configMode = nextConfigMode;
526
550
  }
527
551
  if (resolvedMainTab && mainTabSet.has(resolvedMainTab) && resolvedMainTab !== this.mainTab) {
@@ -710,6 +734,19 @@ document.addEventListener('DOMContentLoaded', () => {
710
734
  this.clearSessionTimelineRefs();
711
735
  },
712
736
 
737
+ watch: {
738
+ mainTab(newTab) {
739
+ if (newTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
740
+ this.loadPromptsContent();
741
+ }
742
+ },
743
+ promptsSubTab() {
744
+ if (this.mainTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
745
+ this.loadPromptsContent();
746
+ }
747
+ }
748
+ },
749
+
713
750
  computed: createAppComputed(),
714
751
  methods: createAppMethods()
715
752
  };
package/web-ui/index.html CHANGED
@@ -16,6 +16,7 @@
16
16
  <!-- @include ./partials/index/panel-config-codex.html -->
17
17
  <!-- @include ./partials/index/panel-config-claude.html -->
18
18
  <!-- @include ./partials/index/panel-config-openclaw.html -->
19
+ <!-- @include ./partials/index/panel-config-opencode.html -->
19
20
  <!-- @include ./partials/index/panel-sessions.html -->
20
21
  <!-- @include ./partials/index/panel-usage.html -->
21
22
  <!-- @include ./partials/index/panel-orchestration.html -->
@@ -24,6 +25,7 @@
24
25
  <!-- @include ./partials/index/panel-trash.html -->
25
26
  <!-- @include ./partials/index/panel-market.html -->
26
27
  <!-- @include ./partials/index/panel-plugins.html -->
28
+ <!-- @include ./partials/index/panel-prompts.html -->
27
29
  <!-- @include ./partials/index/layout-footer.html -->
28
30
  <!-- @include ./partials/index/modals-basic.html -->
29
31
  <!-- @include ./partials/index/modal-webhook.html -->
@@ -4,6 +4,7 @@ import { createSessionComputed } from './app.computed.session.mjs';
4
4
  import { createConfigModeComputed } from './config-mode.computed.mjs';
5
5
  import { createSkillsComputed } from './skills.computed.mjs';
6
6
  import { createPluginsComputed } from './plugins.computed.mjs';
7
+ import { createPromptsComputed } from './app.computed.prompts.mjs';
7
8
 
8
9
  export function createAppComputed() {
9
10
  return {
@@ -12,6 +13,7 @@ export function createAppComputed() {
12
13
  ...createMainTabsComputed(),
13
14
  ...createSkillsComputed(),
14
15
  ...createPluginsComputed(),
15
- ...createConfigModeComputed()
16
+ ...createConfigModeComputed(),
17
+ ...createPromptsComputed()
16
18
  };
17
19
  }