@qthinker/opencode-deepseek-search 0.2.0 → 0.3.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.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -21
- package/README.md +26 -10
- package/gui-experimental.cjs +1 -1
- package/package.json +88 -87
- package/src/deepseek.ts +65 -47
- package/src/gui/index.tsx +14 -7
- package/src/gui/panel.tsx +13 -8
- package/src/i18n/en.ts +151 -0
- package/src/i18n/index.ts +87 -0
- package/src/i18n/ja.ts +138 -0
- package/src/i18n/zh.ts +133 -0
- package/src/index.ts +58 -41
- package/src/rpc.ts +34 -2
- package/src/status-view.ts +59 -51
- package/src/tui/card.ts +4 -1
- package/src/tui/main.ts +16 -8
- package/src/usage.ts +32 -9
package/src/i18n/ja.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// 日本語の文言。状態語は体言止め、文はです・ます調で短く。
|
|
2
|
+
//
|
|
3
|
+
// キー集合は ./en.ts の `Key` で型付けされており、欠けても増えてもコンパイル
|
|
4
|
+
// エラーになる。製品名・Search / Fetch は英語のまま使う。
|
|
5
|
+
|
|
6
|
+
import type { Messages } from "./en.ts"
|
|
7
|
+
|
|
8
|
+
export const ja = {
|
|
9
|
+
// ---------- サイドバーの状態:完全な語と、幅が足りないときの短縮語 ----------
|
|
10
|
+
"state.loading": "読み込み中…",
|
|
11
|
+
"state.loadingShort": "読み込み中…",
|
|
12
|
+
"state.failed": "読み込み失敗",
|
|
13
|
+
"state.failedShort": "失敗",
|
|
14
|
+
"state.enabled": "有効",
|
|
15
|
+
"state.enabledShort": "有効",
|
|
16
|
+
"state.disabled": "無効",
|
|
17
|
+
"state.disabledShort": "無効",
|
|
18
|
+
"state.unpatched": "websearch 未引き継ぎ",
|
|
19
|
+
"state.unpatchedShort": "未引き継ぎ",
|
|
20
|
+
"state.missingKey": "APIキー未設定",
|
|
21
|
+
"state.missingKeyShort": "キー未設定",
|
|
22
|
+
|
|
23
|
+
// ---------- カード上の操作 ----------
|
|
24
|
+
"action.disable": "停止",
|
|
25
|
+
"action.enable": "有効化",
|
|
26
|
+
"action.release": "返上",
|
|
27
|
+
"action.retry": "再試行",
|
|
28
|
+
|
|
29
|
+
// ---------- デスクトップパネル ----------
|
|
30
|
+
"panel.tooltip.refresh": "使用量を更新",
|
|
31
|
+
"panel.loading": "使用量を読み込み中…",
|
|
32
|
+
"panel.label.on": "停止(ネイティブ検索に返上)",
|
|
33
|
+
"panel.label.off": "有効化(DeepSeek が引き継ぐ)",
|
|
34
|
+
"panel.label.warn": "ネイティブ検索に返上",
|
|
35
|
+
"panel.label.error": "再試行",
|
|
36
|
+
"panel.label.idle": "再試行",
|
|
37
|
+
|
|
38
|
+
// ---------- ホスト側の表示:コマンド、メニュー、トースト、ダイアログ ----------
|
|
39
|
+
"command.open": "DeepSeek: 使用量パネルを開く",
|
|
40
|
+
"command.toggle": "DeepSeek: 検索の引き継ぎを切り替え",
|
|
41
|
+
"menu.description": "検索使用量と引き継ぎスイッチ",
|
|
42
|
+
"toast.openSessionFirst": "先にセッションを開いてください",
|
|
43
|
+
"toast.statusMalformed": "状態データの形式が不正です",
|
|
44
|
+
"toast.modeMalformed": "モード切り替え結果の形式が不正です",
|
|
45
|
+
"dialog.title": "DeepSeek 検索",
|
|
46
|
+
"dialog.enable": "有効化",
|
|
47
|
+
"dialog.enableDesc": "組み込み websearch を引き継ぐ",
|
|
48
|
+
"dialog.disable": "停止",
|
|
49
|
+
"dialog.disableDesc": "組み込み websearch をホストに返上",
|
|
50
|
+
"dialog.refresh": "更新",
|
|
51
|
+
"dialog.refreshDesc": "状態と使用量を再読み込み",
|
|
52
|
+
|
|
53
|
+
// ---------- deepseek_mode の出力 ----------
|
|
54
|
+
"mode.report": "現在の検索バックエンド: {mode}(ソース: {source}){suffix}",
|
|
55
|
+
"mode.source.override": "deepseek_mode スイッチ(永続化済み)",
|
|
56
|
+
"mode.source.config": "設定ファイルの options.mode",
|
|
57
|
+
"mode.suffix.patched": " · 組み込み websearch は DeepSeek が処理",
|
|
58
|
+
"mode.suffix.notPatched": " · 組み込み websearch が見つからず、引き継げませんでした",
|
|
59
|
+
"mode.suffix.native": " · 組み込み websearch はネイティブのまま",
|
|
60
|
+
"mode.notFound":
|
|
61
|
+
"組み込み {tool} ツールが見つからないため、実行中は切り替えできません。" +
|
|
62
|
+
"設定ファイルで options.mode を「{mode}」にしてください。",
|
|
63
|
+
"mode.configCleared": "永続化スイッチを削除し、設定ファイルの値({mode})に戻しました。",
|
|
64
|
+
"mode.persisted":
|
|
65
|
+
"永続化しました: OpenCode を再起動しても {mode} のままです。" +
|
|
66
|
+
"設定ファイルの値に戻すには、deepseek_mode に mode: \"config\" を渡してください。",
|
|
67
|
+
"mode.statusOnly": "status は現在のモードを読むだけで、切り替えは行いません。",
|
|
68
|
+
"mode.invalid": "mode は status / deepseek / native / config のいずれかです。受信値: {raw}",
|
|
69
|
+
|
|
70
|
+
// ---------- 進捗と記録の通知 ----------
|
|
71
|
+
"progress.fetch": "DeepSeek Fetch 取得中: {url}",
|
|
72
|
+
"fetch.source": "出典: {url}",
|
|
73
|
+
"usage.reset": "ローカル統計をクリアしました。",
|
|
74
|
+
|
|
75
|
+
// ---------- deepseek_usage のレポート ----------
|
|
76
|
+
"usage.report.title": "DeepSeek 使用量(ローカル集計・成功リクエストで概算。現在のモード {mode}、単価 ¥{price}/1000回)",
|
|
77
|
+
"usage.report.line":
|
|
78
|
+
"{label}: 検索 {search} · 取得 {fetch} · 空/失敗 {skipped}(空結果 {empty} / 失敗 {failed})· 推定 {cost}",
|
|
79
|
+
"usage.report.note":
|
|
80
|
+
"注: 空結果と失敗は課金されません。請求は DeepSeek のアカウント側が正で、この集計は他のマシンや他ツールの呼び出しを含みません。",
|
|
81
|
+
"usage.label.today": "今日",
|
|
82
|
+
"usage.label.month": "今月",
|
|
83
|
+
"usage.label.total": "合計",
|
|
84
|
+
"usage.summary.line": "検索 {search} · 取得 {fetch} · 空/失敗 {skipped} · {cost}",
|
|
85
|
+
|
|
86
|
+
// ---------- エラー ----------
|
|
87
|
+
"error.missingKey":
|
|
88
|
+
"DeepSeek APIキーが未設定です。環境変数 DEEPSEEK_API_KEY を設定してください" +
|
|
89
|
+
"(PowerShell: [Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\",\"sk-...\",\"User\"))。" +
|
|
90
|
+
"またはプラグイン options に \"apiKey\": \"{env:DEEPSEEK_API_KEY}\" を書き、opencode を再起動してください。",
|
|
91
|
+
"error.http.400":
|
|
92
|
+
"DeepSeek のリクエストパラメータが不正です(400){upstream}。検索語は 1〜128 文字、Fetch の URL は有効な http(s) 絶対 URL が必要です。",
|
|
93
|
+
"error.http.401": "DeepSeek APIキーが無効です(401){upstream}。DEEPSEEK_API_KEY 環境変数か options.apiKey を確認してください。",
|
|
94
|
+
"error.http.402": "DeepSeek アカウントの残高が不足しています(402)。失敗したリクエストは課金されません。チャージ後に再試行してください。",
|
|
95
|
+
"error.http.403": "DeepSeek Fetch は対象サイトのセキュリティポリシーに拒否されました(403){upstream}。{hint}",
|
|
96
|
+
"error.http.404": "DeepSeek はこのリンクの本文を抽出できませんでした(404){upstream}。リンクにアクセスできるか確認してください。{hint}",
|
|
97
|
+
"error.http.408": "DeepSeek リクエストがタイムアウトしました(408)。バックオフ再試行も失敗しました。しばらくしてから再試行してください。",
|
|
98
|
+
"error.http.413": "DeepSeek リクエストの本文が大きすぎます(413)。入力を短くしてください。",
|
|
99
|
+
"error.http.429":
|
|
100
|
+
"DeepSeek のレート制限に達しました(429。アカウント上限: Search 100 QPS / Fetch 25 QPS)。" +
|
|
101
|
+
"バックオフ再試行も失敗しました。しばらくしてから再試行してください。",
|
|
102
|
+
"error.http.5xx":
|
|
103
|
+
"DeepSeek のサーバーエラーです({status})。バックオフ再試行も失敗しました。" +
|
|
104
|
+
"一時的に options.mode を \"native\" にするとネイティブ検索を使えます。",
|
|
105
|
+
"error.http.other": "DeepSeek リクエストが失敗しました(HTTP {status}){upstream}",
|
|
106
|
+
"error.http.fetchHint": " このページには組み込み webfetch ツールを使えます。",
|
|
107
|
+
"error.parse": "DeepSeek が解析できないレスポンスを返しました(HTTP {status})。しばらくしてから再試行してください。",
|
|
108
|
+
"error.aborted": "DeepSeek リクエストはキャンセルされました。",
|
|
109
|
+
"error.timeout": "DeepSeek リクエストがタイムアウトしました({seconds} 秒)。再試行してください。",
|
|
110
|
+
"error.network": "{base} に接続できません(ネットワークエラー: {message})。ネットワークかプロキシの設定を確認してください。",
|
|
111
|
+
"error.validate.queryEmpty": "検索語が空です。DeepSeek Search には空でない query(1〜128 文字)が必要です。",
|
|
112
|
+
"error.validate.queryLong": "検索語が長すぎます({length} 文字、上限 {max})。短くして再試行してください。",
|
|
113
|
+
"error.validate.urlScheme": "無効な URL:「{url}」。DeepSeek Fetch は http/https の絶対 URL のみ対応します。",
|
|
114
|
+
"error.validate.urlLong": "URL が長すぎます({length} > {max} 文字)。",
|
|
115
|
+
"error.validate.urlHost": "URL にホスト名がありません:「{url}」。",
|
|
116
|
+
"error.validate.urlUserinfo": "URL にユーザー名/パスワード(userinfo)を含められません。",
|
|
117
|
+
"error.validate.urlParse": "URL を解析できません:「{url}」。",
|
|
118
|
+
"error.fetch.truncated": "[DeepSeek Fetch 切り詰め] 元の本文は {raw} 文字で、上限 {max} 文字を超えています。先頭 {max} 文字のみ保持しました。",
|
|
119
|
+
} satisfies Messages
|
|
120
|
+
|
|
121
|
+
/** 使用量行の階段:完全な行から最も短い行まで。各段は単体で完結する。 */
|
|
122
|
+
export const jaUsageForms: readonly string[] = [
|
|
123
|
+
"{label} Search {search} Fetch {fetch} {cost}",
|
|
124
|
+
"{label} Search {search} Fetch {fetch} {cost}",
|
|
125
|
+
"{label} Search {search} Fetch {fetch}",
|
|
126
|
+
"{label} Search {search} Fetch {fetch}",
|
|
127
|
+
"{label} {search}S {fetch}F {cost}",
|
|
128
|
+
"{label} {search}S {fetch}F",
|
|
129
|
+
"{label} {search}S",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
/** APIキー未設定のヒント階段:最後の段でも何を設定するかは残る。 */
|
|
133
|
+
export const jaHintForms: readonly string[] = [
|
|
134
|
+
"DEEPSEEK_API_KEY を設定して再起動",
|
|
135
|
+
"DEEPSEEK_API_KEY を設定",
|
|
136
|
+
"API KEY を設定",
|
|
137
|
+
"KEY 設定",
|
|
138
|
+
]
|
package/src/i18n/zh.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// 简体中文文案(现有界面文案的升格版,措辞尽量保持原样)。
|
|
2
|
+
//
|
|
3
|
+
// 键集合由 ./en.ts 的 `Key` 约束:缺键或多键都会编译失败。
|
|
4
|
+
|
|
5
|
+
import type { Messages } from "./en.ts"
|
|
6
|
+
|
|
7
|
+
export const zh = {
|
|
8
|
+
// ---------- 侧边栏状态:完整措辞与窄宽度下的短式 ----------
|
|
9
|
+
"state.loading": "正在读取…",
|
|
10
|
+
"state.loadingShort": "正在读取…",
|
|
11
|
+
"state.failed": "读取失败",
|
|
12
|
+
"state.failedShort": "读取失败",
|
|
13
|
+
"state.enabled": "已启用",
|
|
14
|
+
"state.enabledShort": "已启用",
|
|
15
|
+
"state.disabled": "已停用",
|
|
16
|
+
"state.disabledShort": "已停用",
|
|
17
|
+
"state.unpatched": "未接管内置 websearch",
|
|
18
|
+
"state.unpatchedShort": "未接管",
|
|
19
|
+
"state.missingKey": "未配置 API Key",
|
|
20
|
+
"state.missingKeyShort": "缺 API Key",
|
|
21
|
+
|
|
22
|
+
// ---------- 卡片上的可点击动作 ----------
|
|
23
|
+
"action.disable": "停用",
|
|
24
|
+
"action.enable": "启用",
|
|
25
|
+
"action.release": "交还",
|
|
26
|
+
"action.retry": "重试",
|
|
27
|
+
|
|
28
|
+
// ---------- 桌面面板 ----------
|
|
29
|
+
"panel.tooltip.refresh": "刷新用量",
|
|
30
|
+
"panel.loading": "正在读取用量…",
|
|
31
|
+
"panel.label.on": "停用(交还原生搜索)",
|
|
32
|
+
"panel.label.off": "启用(由 DeepSeek 接管)",
|
|
33
|
+
"panel.label.warn": "交还原生搜索",
|
|
34
|
+
"panel.label.error": "重试",
|
|
35
|
+
"panel.label.idle": "重试",
|
|
36
|
+
|
|
37
|
+
// ---------- 宿主 chrome:命令、菜单、toast、对话框 ----------
|
|
38
|
+
"command.open": "DeepSeek: 打开统计面板",
|
|
39
|
+
"command.toggle": "DeepSeek: 启用/停用搜索接管",
|
|
40
|
+
"menu.description": "搜索用量与接管开关",
|
|
41
|
+
"toast.openSessionFirst": "请先打开一个会话",
|
|
42
|
+
"toast.statusMalformed": "状态数据格式不符",
|
|
43
|
+
"toast.modeMalformed": "模式切换结果格式不符",
|
|
44
|
+
"dialog.title": "DeepSeek 搜索",
|
|
45
|
+
"dialog.enable": "启用",
|
|
46
|
+
"dialog.enableDesc": "接管内置 websearch",
|
|
47
|
+
"dialog.disable": "停用",
|
|
48
|
+
"dialog.disableDesc": "把内置 websearch 交还宿主",
|
|
49
|
+
"dialog.refresh": "刷新",
|
|
50
|
+
"dialog.refreshDesc": "重新读取状态与统计",
|
|
51
|
+
|
|
52
|
+
// ---------- deepseek_mode 输出 ----------
|
|
53
|
+
"mode.report": "当前搜索后端: {mode}(来源: {source}){suffix}",
|
|
54
|
+
"mode.source.override": "deepseek_mode 开关,已持久化",
|
|
55
|
+
"mode.source.config": "配置文件 options.mode",
|
|
56
|
+
"mode.suffix.patched": " · 内置 websearch 已由 DeepSeek 接管",
|
|
57
|
+
"mode.suffix.notPatched": " · 未找到内置 websearch 工具,实际未能接管",
|
|
58
|
+
"mode.suffix.native": " · 内置 websearch 保持原生",
|
|
59
|
+
"mode.notFound":
|
|
60
|
+
"未找到内置 {tool} 工具,无法在运行时切换。" +
|
|
61
|
+
'请改用配置文件把 options.mode 设为 "{mode}"。',
|
|
62
|
+
"mode.configCleared": "已清除持久化开关,回到配置文件设置({mode})。",
|
|
63
|
+
"mode.persisted":
|
|
64
|
+
"已持久化:重启 OpenCode 后仍为 {mode}。" +
|
|
65
|
+
'要恢复配置文件设置,调用 deepseek_mode 并传 mode: "config"。',
|
|
66
|
+
"mode.statusOnly": "status 只读取当前模式,不做切换。",
|
|
67
|
+
"mode.invalid": "mode 只能是 status / deepseek / native / config,收到:{raw}",
|
|
68
|
+
|
|
69
|
+
// ---------- 工具进度与记账提示 ----------
|
|
70
|
+
"progress.fetch": "DeepSeek Fetch 抓取中: {url}",
|
|
71
|
+
"fetch.source": "来源: {url}",
|
|
72
|
+
"usage.reset": "已清零本地统计。",
|
|
73
|
+
|
|
74
|
+
// ---------- deepseek_usage 报告 ----------
|
|
75
|
+
"usage.report.title": "DeepSeek 用量(本地统计,按成功请求估算;当前模式 {mode};单价 ¥{price}/1000 次)",
|
|
76
|
+
"usage.report.line":
|
|
77
|
+
"{label}: 搜索 {search} · 抓取 {fetch} · 空/失 {skipped}(空结果 {empty} / 失败 {failed})· 估算 {cost}",
|
|
78
|
+
"usage.report.note": "说明:空结果与失败请求不计费;DeepSeek 以账号侧账单为准,本统计不含其他机器或其他工具的调用。",
|
|
79
|
+
"usage.label.today": "今日",
|
|
80
|
+
"usage.label.month": "本月",
|
|
81
|
+
"usage.label.total": "累计",
|
|
82
|
+
"usage.summary.line": "搜索 {search} · 抓取 {fetch} · 空/失 {skipped} · {cost}",
|
|
83
|
+
|
|
84
|
+
// ---------- 错误信息 ----------
|
|
85
|
+
"error.missingKey":
|
|
86
|
+
"未配置 DeepSeek API Key:请设置环境变量 DEEPSEEK_API_KEY" +
|
|
87
|
+
'(PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User")),' +
|
|
88
|
+
'或在插件 options 中写 "apiKey": "{env:DEEPSEEK_API_KEY}",然后重启 opencode。',
|
|
89
|
+
"error.http.400":
|
|
90
|
+
"DeepSeek 请求参数无效(400){upstream}。搜索词需 1~128 字符;Fetch 的 URL 需为合法的 http(s) 绝对地址。",
|
|
91
|
+
"error.http.401": "DeepSeek API Key 无效(401){upstream}。请检查 DEEPSEEK_API_KEY 环境变量或 options.apiKey。",
|
|
92
|
+
"error.http.402": "DeepSeek 账户余额不足(402),失败请求不计费。请充值后重试。",
|
|
93
|
+
"error.http.403": "DeepSeek Fetch 被目标站点安全策略拒绝(403){upstream}。{hint}",
|
|
94
|
+
"error.http.404": "DeepSeek 无法提取该链接的正文(404){upstream}。请确认链接可访问。{hint}",
|
|
95
|
+
"error.http.408": "DeepSeek 请求超时(408),已退避重试一次仍失败。请稍后重试。",
|
|
96
|
+
"error.http.413": "DeepSeek 请求体过大(413)。请缩短输入。",
|
|
97
|
+
"error.http.429": "DeepSeek 触发限速(429,账号级 Search 100 QPS / Fetch 25 QPS),已退避重试一次仍失败。请稍后再试。",
|
|
98
|
+
"error.http.5xx":
|
|
99
|
+
"DeepSeek 服务端错误({status}),已退避重试一次仍失败。可把插件 options.mode 改为 \"native\" 暂用原生搜索。",
|
|
100
|
+
"error.http.other": "DeepSeek 请求失败(HTTP {status}){upstream}",
|
|
101
|
+
"error.http.fetchHint": " 可改用内置 webfetch 工具抓取该页面。",
|
|
102
|
+
"error.parse": "DeepSeek 返回了无法解析的响应(HTTP {status})。请稍后重试。",
|
|
103
|
+
"error.aborted": "DeepSeek 请求已取消。",
|
|
104
|
+
"error.timeout": "DeepSeek 请求超时({seconds} 秒),请重试。",
|
|
105
|
+
"error.network": "无法连接 {base}(网络错误:{message})。请检查网络或代理设置。",
|
|
106
|
+
"error.validate.queryEmpty": "搜索词为空:DeepSeek Search 需要非空 query(1~128 字符)。",
|
|
107
|
+
"error.validate.queryLong": "搜索词过长({length} 字符,上限 {max}),请精简后重试。",
|
|
108
|
+
"error.validate.urlScheme": "无效 URL:「{url}」。DeepSeek Fetch 仅支持 http/https 绝对地址。",
|
|
109
|
+
"error.validate.urlLong": "URL 过长({length} > {max} 字符)。",
|
|
110
|
+
"error.validate.urlHost": "URL 缺少主机名:「{url}」。",
|
|
111
|
+
"error.validate.urlUserinfo": "URL 不能包含用户名/密码(userinfo)。",
|
|
112
|
+
"error.validate.urlParse": "无法解析 URL:「{url}」。",
|
|
113
|
+
"error.fetch.truncated": "[DeepSeek Fetch 截断提示] 原文共 {raw} 字符,超出上限 {max},此处仅保留前 {max} 字符。",
|
|
114
|
+
} satisfies Messages
|
|
115
|
+
|
|
116
|
+
/** 用量行阶梯:从最完整到最紧凑;每一条都是完整的一行。 */
|
|
117
|
+
export const zhUsageForms: readonly string[] = [
|
|
118
|
+
"{label} Search {search} Fetch {fetch} {cost}",
|
|
119
|
+
"{label} Search {search} Fetch {fetch} {cost}",
|
|
120
|
+
"{label} Search {search} Fetch {fetch}",
|
|
121
|
+
"{label} Search {search} Fetch {fetch}",
|
|
122
|
+
"{label} {search}S {fetch}F {cost}",
|
|
123
|
+
"{label} {search}S {fetch}F",
|
|
124
|
+
"{label} {search}S",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
/** 缺 key 提示阶梯:最后一档仍然指明要设置什么。 */
|
|
128
|
+
export const zhHintForms: readonly string[] = [
|
|
129
|
+
"设置 DEEPSEEK_API_KEY 后重启",
|
|
130
|
+
"设置 API KEY 后重启",
|
|
131
|
+
"设置 API KEY",
|
|
132
|
+
"设置 KEY",
|
|
133
|
+
]
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
validateUrl,
|
|
25
25
|
type SearchResult,
|
|
26
26
|
} from "./deepseek.ts"
|
|
27
|
+
import { resolveLanguage, t } from "./i18n/index.ts"
|
|
27
28
|
import { DeepSeekRpc, type ModeSwitchResult, type SearchStatus } from "./rpc.ts"
|
|
28
29
|
import {
|
|
29
30
|
USAGE_KEY,
|
|
@@ -56,11 +57,13 @@ const SEARCH_TOOL_DESCRIPTION =
|
|
|
56
57
|
"Search the web using DeepSeek Search. Use this for current information beyond knowledge cutoff." +
|
|
57
58
|
`\n\nThe current year is ${new Date().getFullYear()}. Use this year when searching for recent information or current events.`
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
// Pure parser: the caller decides how to surface an invalid value (tool error
|
|
61
|
+
// vs. RPC result), so the message can be localized at that call site.
|
|
62
|
+
function readModeAction(input: unknown): { action: ModeAction } | { invalid: string } {
|
|
60
63
|
const raw = (input as { mode?: unknown } | undefined)?.mode
|
|
61
|
-
if (raw === undefined || raw === null || raw === "") return "status"
|
|
62
|
-
if (raw === "status" || raw === "deepseek" || raw === "native" || raw === "config") return raw
|
|
63
|
-
|
|
64
|
+
if (raw === undefined || raw === null || raw === "") return { action: "status" }
|
|
65
|
+
if (raw === "status" || raw === "deepseek" || raw === "native" || raw === "config") return { action: raw }
|
|
66
|
+
return { invalid: String(raw) }
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
|
|
@@ -95,6 +98,18 @@ export default Plugin.define({
|
|
|
95
98
|
id: "deepseek-search",
|
|
96
99
|
async setup(ctx) {
|
|
97
100
|
const opt = (ctx.options ?? {}) as Record<string, unknown>
|
|
101
|
+
// Resolved once per setup: every tool output and the RPC status payload use
|
|
102
|
+
// this single language, so surfaces can never disagree.
|
|
103
|
+
const pick = resolveLanguage(opt.language, {
|
|
104
|
+
LC_ALL: readEnvValue("LC_ALL"),
|
|
105
|
+
LANG: readEnvValue("LANG"),
|
|
106
|
+
})
|
|
107
|
+
const language = pick.language
|
|
108
|
+
if (pick.problem !== undefined)
|
|
109
|
+
console.error(
|
|
110
|
+
`[deepseek-search] invalid language option ${JSON.stringify(pick.problem)}; ` +
|
|
111
|
+
`valid values: en / ja / zh-CN. Using ${language}.`,
|
|
112
|
+
)
|
|
98
113
|
const configMode: Mode = str(opt.mode) === "native" ? "native" : "deepseek"
|
|
99
114
|
// Precedence: a persisted `deepseek_mode` switch wins over the config file
|
|
100
115
|
// until it is cleared with `mode: "config"`.
|
|
@@ -143,10 +158,10 @@ export default Plugin.define({
|
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
const runSearch = async (query: string, signal: AbortSignal): Promise<SearchResult[]> => {
|
|
146
|
-
const problem = validateQuery(query)
|
|
161
|
+
const problem = validateQuery(query, language)
|
|
147
162
|
if (problem) throw new Error(problem)
|
|
148
163
|
try {
|
|
149
|
-
const results = await deepseekSearch(query, apiKey, count, signal)
|
|
164
|
+
const results = await deepseekSearch(query, apiKey, count, language, signal)
|
|
150
165
|
tally(results.length > 0 ? "search" : "empty")
|
|
151
166
|
return results
|
|
152
167
|
} catch (e) {
|
|
@@ -176,7 +191,7 @@ export default Plugin.define({
|
|
|
176
191
|
|
|
177
192
|
const deepseekSearchExecute = async (input: unknown, context: ToolContext): Promise<ToolResult> => {
|
|
178
193
|
const query = readQuery(input)
|
|
179
|
-
const problem = validateQuery(query)
|
|
194
|
+
const problem = validateQuery(query, language)
|
|
180
195
|
if (problem) throw new Error(problem)
|
|
181
196
|
await ctx.permission.assert({
|
|
182
197
|
sessionID: context.sessionID,
|
|
@@ -217,38 +232,38 @@ export default Plugin.define({
|
|
|
217
232
|
return true
|
|
218
233
|
}
|
|
219
234
|
|
|
220
|
-
const describeMode = (): string =>
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
?
|
|
225
|
-
|
|
226
|
-
|
|
235
|
+
const describeMode = (): string => {
|
|
236
|
+
const source = overrideMode ? t(language, "mode.source.override") : t(language, "mode.source.config")
|
|
237
|
+
const suffix =
|
|
238
|
+
mode === "deepseek"
|
|
239
|
+
? searchToolPatched
|
|
240
|
+
? t(language, "mode.suffix.patched")
|
|
241
|
+
: t(language, "mode.suffix.notPatched")
|
|
242
|
+
: t(language, "mode.suffix.native")
|
|
243
|
+
return t(language, "mode.report", { mode, source, suffix })
|
|
244
|
+
}
|
|
227
245
|
|
|
228
246
|
// Shared by the `deepseek_mode` tool and the RPC `setMode` method. The
|
|
229
247
|
// returned strings are the tool's documented output, word for word.
|
|
230
248
|
const switchMode = async (action: Exclude<ModeAction, "status">): Promise<string> => {
|
|
231
249
|
if (!pristine) {
|
|
232
|
-
return (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
)
|
|
250
|
+
return t(language, "mode.notFound", {
|
|
251
|
+
tool: SEARCH_TOOL,
|
|
252
|
+
mode: action === "config" ? configMode : action,
|
|
253
|
+
})
|
|
236
254
|
}
|
|
237
255
|
if (action === "config") {
|
|
238
256
|
await ctx.storage.remove(MODE_KEY)
|
|
239
257
|
overrideMode = undefined
|
|
240
258
|
await applySearchMode(configMode)
|
|
241
259
|
notifyChanged()
|
|
242
|
-
return `${describeMode()}\n\n
|
|
260
|
+
return `${describeMode()}\n\n${t(language, "mode.configCleared", { mode: configMode })}`
|
|
243
261
|
}
|
|
244
262
|
await ctx.storage.set(MODE_KEY, action)
|
|
245
263
|
overrideMode = action
|
|
246
264
|
await applySearchMode(action)
|
|
247
265
|
notifyChanged()
|
|
248
|
-
return (
|
|
249
|
-
`${describeMode()}\n\n已持久化:重启 OpenCode 后仍为 ${action}。` +
|
|
250
|
-
'要恢复配置文件设置,调用 deepseek_mode 并传 mode: "config"。'
|
|
251
|
-
)
|
|
266
|
+
return `${describeMode()}\n\n${t(language, "mode.persisted", { mode: action })}`
|
|
252
267
|
}
|
|
253
268
|
|
|
254
269
|
// ---------- tools ----------
|
|
@@ -289,11 +304,11 @@ export default Plugin.define({
|
|
|
289
304
|
},
|
|
290
305
|
execute: async (input, context) => {
|
|
291
306
|
const url = String((input as { url?: unknown } | undefined)?.url ?? "").trim()
|
|
292
|
-
const problem = validateUrl(url)
|
|
307
|
+
const problem = validateUrl(url, language)
|
|
293
308
|
if (problem) throw new Error(problem)
|
|
294
309
|
try {
|
|
295
|
-
await context.progress({ status:
|
|
296
|
-
const result = await deepseekFetch(url, apiKey, fetchMaxChars, context.signal)
|
|
310
|
+
await context.progress({ status: t(language, "progress.fetch", { url }) })
|
|
311
|
+
const result = await deepseekFetch(url, apiKey, fetchMaxChars, language, context.signal)
|
|
297
312
|
tally(result.empty ? "empty" : "fetch")
|
|
298
313
|
return {
|
|
299
314
|
content: result.markdown,
|
|
@@ -322,10 +337,10 @@ export default Plugin.define({
|
|
|
322
337
|
execute: async (input) => {
|
|
323
338
|
const reset = Boolean((input as { reset?: unknown } | undefined)?.reset)
|
|
324
339
|
const report = await withLedger(async (ledger) => {
|
|
325
|
-
const text = formatUsage(ledger, pricePerThousand, mode)
|
|
340
|
+
const text = formatUsage(ledger, pricePerThousand, mode, language)
|
|
326
341
|
if (!reset) return text
|
|
327
342
|
await ctx.storage.set(USAGE_KEY, emptyLedger())
|
|
328
|
-
return `${text}\n\n
|
|
343
|
+
return `${text}\n\n${t(language, "usage.reset")}`
|
|
329
344
|
})
|
|
330
345
|
return { content: report }
|
|
331
346
|
},
|
|
@@ -351,7 +366,9 @@ export default Plugin.define({
|
|
|
351
366
|
additionalProperties: false,
|
|
352
367
|
},
|
|
353
368
|
execute: async (input) => {
|
|
354
|
-
const
|
|
369
|
+
const parsed = readModeAction(input)
|
|
370
|
+
if ("invalid" in parsed) throw new Error(t(language, "mode.invalid", { raw: parsed.invalid }))
|
|
371
|
+
const action = parsed.action
|
|
355
372
|
if (action === "status") return { content: describeMode() }
|
|
356
373
|
return { content: await switchMode(action) }
|
|
357
374
|
},
|
|
@@ -372,7 +389,8 @@ export default Plugin.define({
|
|
|
372
389
|
patched: searchToolPatched,
|
|
373
390
|
hasKey: Boolean(apiKey),
|
|
374
391
|
pricePerThousand,
|
|
375
|
-
|
|
392
|
+
language,
|
|
393
|
+
...usageSummaryLines(ledger, language, todayKey),
|
|
376
394
|
// Same ledger read as the strings above, exposed as numbers so the
|
|
377
395
|
// sidebars can lay them out for their own width.
|
|
378
396
|
counts: {
|
|
@@ -384,13 +402,12 @@ export default Plugin.define({
|
|
|
384
402
|
}),
|
|
385
403
|
// The input is plain JSON Schema, hence `unknown`: report, never throw.
|
|
386
404
|
setMode: async (input: unknown): Promise<ModeSwitchResult> => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
} catch (e) {
|
|
391
|
-
return { ok: false, mode, message: e instanceof Error ? e.message : String(e) }
|
|
405
|
+
const parsed = readModeAction(input)
|
|
406
|
+
if ("invalid" in parsed) {
|
|
407
|
+
return { ok: false, mode, message: t(language, "mode.invalid", { raw: parsed.invalid }) }
|
|
392
408
|
}
|
|
393
|
-
|
|
409
|
+
const action = parsed.action
|
|
410
|
+
if (action === "status") return { ok: false, mode, message: t(language, "mode.statusOnly") }
|
|
394
411
|
const message = await switchMode(action)
|
|
395
412
|
// `!pristine` means switchMode could not apply the change; its message says why.
|
|
396
413
|
return { ok: Boolean(pristine), mode, message }
|
|
@@ -403,16 +420,16 @@ export default Plugin.define({
|
|
|
403
420
|
`apiKey=${apiKey ? "present" : "MISSING"} count=${count} ` +
|
|
404
421
|
`searchTool=${searchToolPatched ? "deepseek" : "untouched"}`,
|
|
405
422
|
)
|
|
406
|
-
if (!apiKey) console.error(`[deepseek-search] ${missingKeyMessage()}`)
|
|
423
|
+
if (!apiKey) console.error(`[deepseek-search] ${missingKeyMessage("en")}`)
|
|
407
424
|
if (overrideMode && overrideMode !== configMode)
|
|
408
425
|
console.error(
|
|
409
|
-
`[deepseek-search]
|
|
410
|
-
|
|
426
|
+
`[deepseek-search] note: a persisted mode=${overrideMode} overrides the config file's ${configMode}; ` +
|
|
427
|
+
`call deepseek_mode with "config" to clear it.`,
|
|
411
428
|
)
|
|
412
429
|
if (mode === "deepseek" && !searchToolPatched)
|
|
413
430
|
console.error(
|
|
414
|
-
`[deepseek-search]
|
|
415
|
-
|
|
431
|
+
`[deepseek-search] warning: built-in ${SEARCH_TOOL} tool not found; search still uses the built-in backend. ` +
|
|
432
|
+
`Set "websearch": { "provider": "${PROVIDER_ID}" } in opencode.jsonc to force the route.`,
|
|
416
433
|
)
|
|
417
434
|
},
|
|
418
435
|
})
|
package/src/rpc.ts
CHANGED
|
@@ -13,6 +13,17 @@ import type { Rpc } from "@opencode/plugin/rpc"
|
|
|
13
13
|
|
|
14
14
|
export const DEEPSEEK_RPC_ID = "deepseek-search"
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* UI languages the plugin ships. The server resolves one of them once and puts
|
|
18
|
+
* it in `status`, so the tools and both sidebars always speak the same language.
|
|
19
|
+
*/
|
|
20
|
+
export type Language = "en" | "ja" | "zh-CN"
|
|
21
|
+
export const LANGUAGE_VALUES = ["en", "ja", "zh-CN"] as const
|
|
22
|
+
|
|
23
|
+
export function isLanguage(value: unknown): value is Language {
|
|
24
|
+
return value === "en" || value === "ja" || value === "zh-CN"
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
// Shared by the today/month/total slots of `status.counts`.
|
|
17
28
|
const USAGE_COUNTS_SCHEMA = {
|
|
18
29
|
type: "object",
|
|
@@ -43,6 +54,9 @@ export const DeepSeekRpc = {
|
|
|
43
54
|
today: { type: "string" },
|
|
44
55
|
month: { type: "string" },
|
|
45
56
|
total: { type: "string" },
|
|
57
|
+
// Declared but not required: older servers omit it, and readStatus
|
|
58
|
+
// falls back to English without failing the rest of the payload.
|
|
59
|
+
language: { type: "string", enum: ["en", "ja", "zh-CN"] },
|
|
46
60
|
counts: {
|
|
47
61
|
type: "object",
|
|
48
62
|
properties: {
|
|
@@ -102,6 +116,12 @@ export interface SearchStatus {
|
|
|
102
116
|
today: string
|
|
103
117
|
month: string
|
|
104
118
|
total: string
|
|
119
|
+
/**
|
|
120
|
+
* UI language resolved by the server. Required by this interface: `readStatus`
|
|
121
|
+
* always fills it (English when the server is older or sends garbage), so
|
|
122
|
+
* every surface has one value to render with.
|
|
123
|
+
*/
|
|
124
|
+
language: Language
|
|
105
125
|
/** Structured counters, so each surface can format for its own width. Optional for older servers. */
|
|
106
126
|
counts?: { today: UsageCounts; month: UsageCounts; total: UsageCounts }
|
|
107
127
|
}
|
|
@@ -134,13 +154,25 @@ function readUsageCounts(value: unknown): UsageCounts | undefined {
|
|
|
134
154
|
/** Defensive read of an RPC `status` result. Never throws. */
|
|
135
155
|
export function readStatus(raw: unknown): SearchStatus | undefined {
|
|
136
156
|
if (!isRecord(raw)) return undefined
|
|
137
|
-
const { mode, source, patched, hasKey, pricePerThousand, today, month, total, counts } = raw
|
|
157
|
+
const { mode, source, patched, hasKey, pricePerThousand, today, month, total, language, counts } = raw
|
|
138
158
|
if (!isMode(mode)) return undefined
|
|
139
159
|
if (source !== "override" && source !== "config") return undefined
|
|
140
160
|
if (typeof patched !== "boolean" || typeof hasKey !== "boolean") return undefined
|
|
141
161
|
if (typeof pricePerThousand !== "number" || !Number.isFinite(pricePerThousand)) return undefined
|
|
142
162
|
if (typeof today !== "string" || typeof month !== "string" || typeof total !== "string") return undefined
|
|
143
|
-
const status: SearchStatus = {
|
|
163
|
+
const status: SearchStatus = {
|
|
164
|
+
mode,
|
|
165
|
+
source,
|
|
166
|
+
patched,
|
|
167
|
+
hasKey,
|
|
168
|
+
pricePerThousand,
|
|
169
|
+
today,
|
|
170
|
+
month,
|
|
171
|
+
total,
|
|
172
|
+
// An older server has no language; a malformed one is not worth failing the
|
|
173
|
+
// whole payload over. Both fall back to English.
|
|
174
|
+
language: isLanguage(language) ? language : "en",
|
|
175
|
+
}
|
|
144
176
|
// Old servers omit `counts`; a malformed triple is dropped without failing the rest.
|
|
145
177
|
if (isRecord(counts)) {
|
|
146
178
|
const todayCounts = readUsageCounts(counts.today)
|