opencode-tbot 0.1.17 → 0.1.20

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/README.ja.md ADDED
@@ -0,0 +1,164 @@
1
+ # opencode-tbot
2
+
3
+ チャットから [OpenCode](https://opencode.ai) を操作するための Telegram プラグインです。
4
+
5
+ [English](./README.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
6
+
7
+ > このプロジェクトは OpenCode チームによって開発されたものではなく、公式な関連もありません。
8
+
9
+ ## 概要
10
+
11
+ `opencode-tbot` を使うと、Telegram から OpenCode を操作できます。
12
+
13
+ - テキストメッセージは現在アクティブな OpenCode セッションに転送されます。
14
+ - Telegram の写真と画像ドキュメントは OpenCode のファイルパートとしてアップロードされます。
15
+ - Telegram の音声メッセージは明示的に拒否され、ローカライズされた返信を返します。
16
+ - OpenCode が発行した権限リクエストは、Telegram のインラインボタンから承認または拒否できます。
17
+ - セッション完了やエラーイベントは、紐付けられた Telegram チャットへ通知できます。
18
+ - チャットの紐付け状態は JSON の state ファイルに保存されます。
19
+
20
+ ## 前提条件
21
+
22
+ - このプラグインを読み込む OpenCode ホストプロセスが動作していること。
23
+ - Telegram bot token を用意していること。
24
+ - CLI とローカル開発には Node.js `>=22.12.0` が必要です。
25
+
26
+ ## インストール
27
+
28
+ 推奨インストール方法:
29
+
30
+ ```bash
31
+ npm exec --package opencode-tbot@latest opencode-tbot -- install
32
+ ```
33
+
34
+ インストーラーはプラグインをグローバルに登録し、デフォルトのランタイム設定を書き込みます。
35
+
36
+ インストール済み CLI のバージョン確認:
37
+
38
+ ```bash
39
+ npm exec --package opencode-tbot@latest opencode-tbot -- --version
40
+ ```
41
+
42
+ OpenCode に登録済みの npm プラグイン spec を更新:
43
+
44
+ ```bash
45
+ npm exec --package opencode-tbot@latest opencode-tbot -- update
46
+ ```
47
+
48
+ ### CLI オプション
49
+
50
+ `install` で利用可能:
51
+
52
+ - `--bot-token <token>` Telegram bot token を非対話で設定
53
+ - `--telegram-api-root <url>` Telegram Bot API のベース URL を上書き
54
+ - `--plugin-spec <spec>` カスタム npm プラグイン spec を登録
55
+ - `--skip-register` OpenCode 側のプラグイン登録は変更せず、プラグイン設定だけを書き換え
56
+ - `--home-dir <path>` カスタム home ディレクトリを使用
57
+
58
+ `update` で利用可能:
59
+
60
+ - `--plugin-spec <spec>`
61
+ - `--home-dir <path>`
62
+
63
+ ## 設定
64
+
65
+ ランタイム設定は次の順序で読み込まれます。
66
+
67
+ 1. `~/.config/opencode/opencode-tbot/config.json` のグローバルデフォルト
68
+ 2. `<worktree>/tbot.config.json` のプロジェクト上書き設定
69
+
70
+ プロジェクト設定はグローバル設定に上書きマージされます。`telegram` と `state` はセクション単位でディープマージされます。
71
+
72
+ 古い `openrouter` 音声転写設定はランタイムでは無視され、インストーラーが設定を書き直す際にも削除されます。
73
+
74
+ リポジトリには最小構成の参考として [tbot.config.example.json](./tbot.config.example.json) も含まれています。
75
+
76
+ ### `tbot.config.json` の例
77
+
78
+ ```json
79
+ {
80
+ "telegram": {
81
+ "botToken": "your_telegram_bot_token",
82
+ "allowedChatIds": [123456789],
83
+ "apiRoot": "https://api.telegram.org"
84
+ },
85
+ "state": {
86
+ "path": "./data/opencode-tbot.state.json"
87
+ },
88
+ "logLevel": "info"
89
+ }
90
+ ```
91
+
92
+ ### フィールド
93
+
94
+ | フィールド | 必須 | デフォルト | 説明 |
95
+ | --- | --- | --- | --- |
96
+ | `telegram.botToken` | はい | - | Telegram bot token。通常はインストーラーがグローバルプラグイン設定へ書き込みます。 |
97
+ | `telegram.allowedChatIds` | いいえ | `[]` | 許可する Telegram chat ID の配列。空の場合はすべての chat を受け付けます。 |
98
+ | `telegram.apiRoot` | いいえ | `https://api.telegram.org` | Telegram Bot API のベース URL。テストやセルフホストのゲートウェイ向けです。 |
99
+ | `state.path` | いいえ | `./data/opencode-tbot.state.json` | JSON state ファイルのパス。現在の OpenCode worktree からの相対パスとして解決されます。 |
100
+ | `logLevel` | いいえ | `info` | プラグインのログレベル。ログは `client.app.log()` 経由で出力されます。 |
101
+
102
+ ### 補足
103
+
104
+ - `state.path` は現在の OpenCode worktree からの相対パスとして解決されます。
105
+ - `telegram.allowedChatIds` を空のままにすると、bot は任意の chat からのメッセージを受け付けます。本番では制限を設定してください。
106
+ - 権限承認とセッション通知はプラグインの hook で処理されます。
107
+ - `/language` は現在 English、简体中文、日本語 をサポートしています。
108
+
109
+ ## クイックスタート
110
+
111
+ 1. `npm exec --package opencode-tbot@latest opencode-tbot -- install` でプラグインをインストールします。
112
+ 2. 特定の chat のみ許可したい場合は、`tbot.config.json` で `telegram.allowedChatIds` を設定します。
113
+ 3. 対象の worktree で OpenCode を起動し、プラグインランタイムを読み込ませます。
114
+ 4. Telegram で `/status` を実行し、接続を確認します。
115
+ 5. `/new [title]` を実行するか、テキストメッセージを直接送信して使い始めます。
116
+
117
+ ## コマンド
118
+
119
+ - `/start` 短いウェルカムメッセージとクイックスタート手順を表示
120
+ - `/status` OpenCode のヘルス、パス、プラグイン、LSP、MCP の状態をまとめて表示
121
+ - `/new [title]` 新しい OpenCode セッションを作成
122
+ - `/agents` または `/agent` 利用可能な agent を一覧表示し、アクティブな agent を切り替え
123
+ - `/sessions` 利用可能なセッションを一覧表示し、アクティブなセッションを切り替え
124
+ - `/cancel` セッション名の変更をキャンセルするか、現在のセッションで実行中のリクエストを中止
125
+ - `/model` または `/models` 利用可能なモデルを一覧表示し、アクティブなモデルを切り替え
126
+ - `/language` bot の表示言語を切り替え
127
+
128
+ メッセージ処理:
129
+
130
+ - コマンド以外のテキストは prompt として扱われ、OpenCode に送信されます。
131
+ - Telegram の写真と画像ドキュメントは OpenCode のファイルパートとして転送されます。
132
+ - Telegram の音声メッセージは未対応で、ローカライズされた拒否メッセージを返します。
133
+
134
+ ## 開発
135
+
136
+ プラグインバンドルをビルド:
137
+
138
+ ```bash
139
+ pnpm build
140
+ ```
141
+
142
+ 型チェック:
143
+
144
+ ```bash
145
+ pnpm typecheck
146
+ ```
147
+
148
+ テスト実行:
149
+
150
+ ```bash
151
+ pnpm test
152
+ ```
153
+
154
+ このリポジトリでソースベースのローカル読み込みを行う場合、OpenCode は [.opencode/plugins/opencode-tbot.ts](./.opencode/plugins/opencode-tbot.ts) を使って `src/plugin.ts` を再エクスポートできます。
155
+
156
+ ## FAQ
157
+
158
+ ### 実行中の OpenCode インスタンスは必要ですか?
159
+
160
+ はい。このリポジトリが提供するのは Telegram 連携レイヤーであり、プラグインを読み込む OpenCode ホストプロセスに依存します。
161
+
162
+ ### これは OpenCode の公式プロジェクトですか?
163
+
164
+ いいえ。OpenCode と連携するプロジェクトですが、OpenCode チームが開発したものではありません。
package/README.md CHANGED
@@ -2,24 +2,30 @@
2
2
 
3
3
  A Telegram plugin for driving [OpenCode](https://opencode.ai) from chat.
4
4
 
5
- [English](./README.md) | [简体中文](./README.zh-CN.md)
5
+ [English](./README.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
6
6
 
7
7
  > This project is not built by the OpenCode team and is not affiliated with them.
8
8
 
9
- ## What It Does
9
+ ## Overview
10
10
 
11
11
  `opencode-tbot` lets you operate OpenCode from Telegram.
12
12
 
13
13
  - Text messages are forwarded to the active OpenCode session.
14
- - Telegram images are uploaded as OpenCode file parts.
15
- - Telegram voice messages can be transcribed through OpenRouter before entering the normal prompt flow.
16
- - Permission requests raised by OpenCode can be approved or rejected directly from Telegram inline buttons.
14
+ - Telegram photos and image documents are uploaded as OpenCode file parts.
15
+ - Telegram voice messages are explicitly rejected with a localized reply.
16
+ - Permission requests raised by OpenCode can be approved or rejected from Telegram inline buttons.
17
17
  - Session completion and error events can be reported back to the bound Telegram chat.
18
- - Chat state is stored in a JSON state file that works in both Node and Bun runtimes.
18
+ - Chat bindings are stored in a JSON state file.
19
+
20
+ ## Requirements
21
+
22
+ - A running OpenCode host process that loads the plugin.
23
+ - A Telegram bot token.
24
+ - Node.js `>=22.12.0` for the CLI and local development workflow.
19
25
 
20
26
  ## Install
21
27
 
22
- Run:
28
+ Recommended install:
23
29
 
24
30
  ```bash
25
31
  npm exec --package opencode-tbot@latest opencode-tbot -- install
@@ -39,20 +45,33 @@ Update the registered npm plugin spec in OpenCode:
39
45
  npm exec --package opencode-tbot@latest opencode-tbot -- update
40
46
  ```
41
47
 
42
- Remove a legacy local npm install if OpenCode is showing the plugin as a `file:///.../node_modules/...` path:
48
+ ### CLI Options
43
49
 
44
- ```bash
45
- npm uninstall opencode-tbot
46
- ```
50
+ `install` supports:
51
+
52
+ - `--bot-token <token>` set the Telegram bot token non-interactively
53
+ - `--telegram-api-root <url>` override the Telegram Bot API root
54
+ - `--plugin-spec <spec>` register a custom npm plugin spec
55
+ - `--skip-register` only rewrite plugin config without touching OpenCode plugin registration
56
+ - `--home-dir <path>` use a custom home directory
57
+
58
+ `update` supports:
59
+
60
+ - `--plugin-spec <spec>`
61
+ - `--home-dir <path>`
47
62
 
48
63
  ## Configuration
49
64
 
50
- The runtime config is loaded in this order:
65
+ Runtime config is loaded in this order:
66
+
67
+ 1. Global defaults from `~/.config/opencode/opencode-tbot/config.json`
68
+ 2. Project overrides from `<worktree>/tbot.config.json`
51
69
 
52
- 1. global defaults from `~/.config/opencode/opencode-tbot/config.json`
53
- 2. project overrides from `<worktree>/tbot.config.json`
70
+ Project config is merged on top of the global config. `telegram` and `state` are deep-merged by section.
54
71
 
55
- Project config is merged on top of the global config. `telegram`, `state`, and `openrouter` are deep-merged by section.
72
+ Legacy `openrouter` voice-transcription settings are ignored at runtime. When the installer rewrites the config, it removes them.
73
+
74
+ The repository also includes [tbot.config.example.json](./tbot.config.example.json) as a minimal reference.
56
75
 
57
76
  ### Example `tbot.config.json`
58
77
 
@@ -66,12 +85,6 @@ Project config is merged on top of the global config. `telegram`, `state`, and `
66
85
  "state": {
67
86
  "path": "./data/opencode-tbot.state.json"
68
87
  },
69
- "openrouter": {
70
- "apiKey": "your_openrouter_api_key",
71
- "model": "openai/gpt-audio-mini",
72
- "timeoutMs": 30000,
73
- "transcriptionPrompt": ""
74
- },
75
88
  "logLevel": "info"
76
89
  }
77
90
  ```
@@ -84,39 +97,27 @@ Project config is merged on top of the global config. `telegram`, `state`, and `
84
97
  | `telegram.allowedChatIds` | No | `[]` | Allowed Telegram chat IDs. If empty, the bot accepts messages from any chat. |
85
98
  | `telegram.apiRoot` | No | `https://api.telegram.org` | Telegram Bot API base URL. Useful for tests or self-hosted gateways. |
86
99
  | `state.path` | No | `./data/opencode-tbot.state.json` | JSON state file path, resolved relative to the current OpenCode worktree. |
87
- | `openrouter.apiKey` | No | `""` | OpenRouter API key. Required only when voice transcription is enabled. |
88
- | `openrouter.model` | No | `openai/gpt-audio-mini` | OpenRouter model for voice transcription. |
89
- | `openrouter.timeoutMs` | No | `30000` | Voice transcription timeout in milliseconds. |
90
- | `openrouter.transcriptionPrompt` | No | `""` | Optional extra instruction appended to the transcription prompt. |
91
100
  | `logLevel` | No | `info` | Plugin log level. Logs are emitted through `client.app.log()`. |
92
101
 
93
- ## Runtime Expectations
94
-
95
- Required:
96
-
97
- - `telegram.botToken`
102
+ ### Notes
98
103
 
99
- Optional:
100
-
101
- - `telegram.allowedChatIds`
102
- - `telegram.apiRoot`
103
- - `state.path`
104
- - `openrouter.apiKey`
105
- - `openrouter.model`
106
- - `openrouter.timeoutMs`
107
- - `openrouter.transcriptionPrompt`
108
- - `logLevel`
104
+ - `state.path` is resolved relative to the current OpenCode worktree.
105
+ - If `telegram.allowedChatIds` is left empty, the bot accepts messages from any chat. Restrict it in production.
106
+ - Permission approvals and session notifications are handled through plugin hooks.
107
+ - `/language` currently supports English, Simplified Chinese, and Japanese.
109
108
 
110
- Notes:
109
+ ## Quick Start
111
110
 
112
- - `state.path` defaults to `./data/opencode-tbot.state.json` and is resolved relative to the current OpenCode worktree.
113
- - Logs are emitted through `client.app.log()`.
114
- - Permission approvals and session notifications are handled through plugin hooks.
111
+ 1. Install the plugin with `npm exec --package opencode-tbot@latest opencode-tbot -- install`.
112
+ 2. Set `telegram.allowedChatIds` in `tbot.config.json` if you want to restrict the bot to specific chats.
113
+ 3. Start OpenCode in the target worktree so the plugin runtime can load.
114
+ 4. In Telegram, run `/status` to verify the connection.
115
+ 5. Run `/new [title]` or send a text message directly to start working.
115
116
 
116
117
  ## Commands
117
118
 
118
119
  - `/start` show a short welcome message and quick-start steps
119
- - `/status` show aggregated OpenCode health, path, LSP, and MCP status
120
+ - `/status` show aggregated OpenCode health, path, plugin, LSP, and MCP status
120
121
  - `/new [title]` create a new OpenCode session
121
122
  - `/agents` or `/agent` list available agents and switch the active one
122
123
  - `/sessions` list available sessions and switch the active one
@@ -124,7 +125,11 @@ Notes:
124
125
  - `/model` or `/models` list available models and switch the active one
125
126
  - `/language` switch the bot display language
126
127
 
127
- Any non-command text message is treated as a prompt and sent to OpenCode. Telegram `voice` messages follow the same prompt flow after transcription when OpenRouter is configured. Telegram images are forwarded as OpenCode file parts.
128
+ Message handling:
129
+
130
+ - Non-command text is treated as a prompt and sent to OpenCode.
131
+ - Telegram photos and image documents are forwarded as OpenCode file parts.
132
+ - Telegram voice messages are not supported and receive a localized rejection reply.
128
133
 
129
134
  ## Development
130
135
 
package/README.zh-CN.md CHANGED
@@ -2,24 +2,30 @@
2
2
 
3
3
  一个通过 Telegram 驱动 [OpenCode](https://opencode.ai) 的插件。
4
4
 
5
- [English](./README.md) | [简体中文](./README.zh-CN.md)
5
+ [English](./README.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
6
6
 
7
7
  > 本项目并非由 OpenCode 团队开发,也不隶属于 OpenCode 官方。
8
8
 
9
- ## 项目说明
9
+ ## 项目概览
10
10
 
11
11
  `opencode-tbot` 允许你直接在 Telegram 中操作 OpenCode。
12
12
 
13
- - 文本消息会转发到当前 OpenCode 会话。
14
- - Telegram 图片会作为 OpenCode 文件片段上传。
15
- - Telegram 语音消息可先通过 OpenRouter 转写,再进入正常 prompt 流程。
16
- - OpenCode 触发的权限请求可以直接在 Telegram 中通过内联按钮批准或拒绝。
17
- - 会话完成和错误事件可以主动回推到绑定的 Telegram chat。
18
- - 聊天状态通过 JSON 状态文件持久化,可兼容 Node 和 Bun 运行环境。
13
+ - 文本消息会转发到当前激活的 OpenCode 会话。
14
+ - Telegram 照片和图片文档会作为 OpenCode 文件片段上传。
15
+ - Telegram 语音消息会被明确拒绝,并返回本地化提示。
16
+ - OpenCode 触发的权限请求可以直接在 Telegram 内联按钮中批准或拒绝。
17
+ - 会话完成和错误事件可以回推到绑定的 Telegram chat。
18
+ - 聊天绑定状态通过 JSON 状态文件持久化。
19
+
20
+ ## 环境要求
21
+
22
+ - 一个正在运行、并会加载该插件的 OpenCode Host 进程。
23
+ - 一个 Telegram bot token。
24
+ - Node.js `>=22.12.0`,用于 CLI 和本地开发流程。
19
25
 
20
26
  ## 安装
21
27
 
22
- 执行:
28
+ 推荐安装方式:
23
29
 
24
30
  ```bash
25
31
  npm exec --package opencode-tbot@latest opencode-tbot -- install
@@ -39,11 +45,20 @@ npm exec --package opencode-tbot@latest opencode-tbot -- --version
39
45
  npm exec --package opencode-tbot@latest opencode-tbot -- update
40
46
  ```
41
47
 
42
- 从当前环境卸载 npm 包:
48
+ ### CLI 参数
43
49
 
44
- ```bash
45
- npm uninstall opencode-tbot
46
- ```
50
+ `install` 支持:
51
+
52
+ - `--bot-token <token>` 非交互式写入 Telegram bot token
53
+ - `--telegram-api-root <url>` 覆盖 Telegram Bot API 根地址
54
+ - `--plugin-spec <spec>` 注册自定义 npm 插件 spec
55
+ - `--skip-register` 只重写插件配置,不改动 OpenCode 的插件注册
56
+ - `--home-dir <path>` 使用自定义 home 目录
57
+
58
+ `update` 支持:
59
+
60
+ - `--plugin-spec <spec>`
61
+ - `--home-dir <path>`
47
62
 
48
63
  ## 配置
49
64
 
@@ -52,7 +67,11 @@ npm uninstall opencode-tbot
52
67
  1. 全局默认配置 `~/.config/opencode/opencode-tbot/config.json`
53
68
  2. 项目覆盖配置 `<worktree>/tbot.config.json`
54
69
 
55
- 项目配置会覆盖全局默认值;`telegram`、`state`、`openrouter` 这些分段配置会进行深合并。
70
+ 项目配置会覆盖全局默认值;`telegram` 和 `state` 会按分段进行深合并。
71
+
72
+ 遗留的 `openrouter` 语音转写配置在运行时会被忽略;安装器重写配置时也会自动移除这些字段。
73
+
74
+ 仓库内还提供了 [tbot.config.example.json](./tbot.config.example.json) 作为最小配置参考。
56
75
 
57
76
  ### `tbot.config.json` 示例
58
77
 
@@ -66,12 +85,6 @@ npm uninstall opencode-tbot
66
85
  "state": {
67
86
  "path": "./data/opencode-tbot.state.json"
68
87
  },
69
- "openrouter": {
70
- "apiKey": "your_openrouter_api_key",
71
- "model": "openai/gpt-audio-mini",
72
- "timeoutMs": 30000,
73
- "transcriptionPrompt": ""
74
- },
75
88
  "logLevel": "info"
76
89
  }
77
90
  ```
@@ -84,39 +97,27 @@ npm uninstall opencode-tbot
84
97
  | `telegram.allowedChatIds` | 否 | `[]` | 允许访问的 Telegram chat ID 数组。为空时表示接受任意 chat。 |
85
98
  | `telegram.apiRoot` | 否 | `https://api.telegram.org` | Telegram Bot API 根地址,适合测试或自托管网关。 |
86
99
  | `state.path` | 否 | `./data/opencode-tbot.state.json` | JSON 状态文件路径,相对当前 OpenCode worktree 解析。 |
87
- | `openrouter.apiKey` | 否 | `""` | OpenRouter API key。仅在启用语音转写时需要。 |
88
- | `openrouter.model` | 否 | `openai/gpt-audio-mini` | 语音转写使用的 OpenRouter 模型。 |
89
- | `openrouter.timeoutMs` | 否 | `30000` | 语音转写超时时间,单位毫秒。 |
90
- | `openrouter.transcriptionPrompt` | 否 | `""` | 追加到内置转写提示词后的可选说明。 |
91
100
  | `logLevel` | 否 | `info` | 插件日志级别。日志统一通过 `client.app.log()` 上报。 |
92
101
 
93
- ## 运行时约定
94
-
95
- 必填:
102
+ ### 说明
96
103
 
97
- - `telegram.botToken`
104
+ - `state.path` 会相对当前 OpenCode worktree 解析。
105
+ - 如果 `telegram.allowedChatIds` 为空,bot 会接受任意 chat 的消息;生产环境建议显式限制。
106
+ - 权限审批和会话通知通过插件 hook 处理。
107
+ - `/language` 当前支持 English、简体中文、日本語。
98
108
 
99
- 可选:
109
+ ## 快速开始
100
110
 
101
- - `telegram.allowedChatIds`
102
- - `telegram.apiRoot`
103
- - `state.path`
104
- - `openrouter.apiKey`
105
- - `openrouter.model`
106
- - `openrouter.timeoutMs`
107
- - `openrouter.transcriptionPrompt`
108
- - `logLevel`
109
-
110
- 说明:
111
-
112
- - `state.path` 默认是 `./data/opencode-tbot.state.json`,并相对当前 OpenCode worktree 解析
113
- - 日志通过 `client.app.log()` 统一输出
114
- - 权限审批和会话通知由插件 hook 处理
111
+ 1. 使用 `npm exec --package opencode-tbot@latest opencode-tbot -- install` 安装插件。
112
+ 2. 如果你只想允许特定聊天使用 bot,请在 `tbot.config.json` 中设置 `telegram.allowedChatIds`。
113
+ 3. 在目标 worktree 中启动 OpenCode,让插件运行时被加载。
114
+ 4. 在 Telegram 中执行 `/status` 验证连接是否正常。
115
+ 5. 执行 `/new [title]`,或者直接发送文本消息开始使用。
115
116
 
116
117
  ## 命令
117
118
 
118
119
  - `/start` 显示简短欢迎信息和快速开始说明
119
- - `/status` 显示 OpenCode 健康状态、路径、LSP 和 MCP 信息
120
+ - `/status` 显示 OpenCode 健康状态、路径、插件、LSP 和 MCP 信息
120
121
  - `/new [title]` 创建新的 OpenCode 会话
121
122
  - `/agents` 或 `/agent` 列出可用 agent 并切换当前 agent
122
123
  - `/sessions` 列出会话并切换当前会话
@@ -124,7 +125,11 @@ npm uninstall opencode-tbot
124
125
  - `/model` 或 `/models` 列出可用模型并切换当前模型
125
126
  - `/language` 切换 bot 显示语言
126
127
 
127
- 任意非命令文本都会被当作 prompt 发送给 OpenCode。配置了 OpenRouter 后,Telegram `voice` 消息会先转写再进入同一条 prompt 流程。图片会作为 OpenCode 文件片段上传。
128
+ 消息处理规则:
129
+
130
+ - 任意非命令文本都会被当作 prompt 发送给 OpenCode。
131
+ - Telegram 照片和图片文档会作为 OpenCode 文件片段上传。
132
+ - Telegram 语音消息当前不受支持,bot 会直接返回本地化拒绝提示。
128
133
 
129
134
  ## 开发
130
135
 
@@ -156,4 +161,4 @@ pnpm test
156
161
 
157
162
  ### 这是 OpenCode 官方项目吗?
158
163
 
159
- 不是。它只是与 OpenCode 集成,并非 OpenCode 官方项目。
164
+ 不是。它只是一个 OpenCode 集成,并非 OpenCode 官方项目。
@@ -5,7 +5,6 @@ import { z } from "zod";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  //#region src/app/config.ts
8
- var DEFAULT_OPENROUTER_MODEL = "openai/gpt-audio-mini";
9
8
  var DEFAULT_STATE_FILE_PATH = "./data/opencode-tbot.state.json";
10
9
  var DEFAULT_TELEGRAM_API_ROOT = "https://api.telegram.org";
11
10
  var AllowedChatIdSchema = z.union([z.number().int(), z.string().regex(/^-?\d+$/u).transform((value) => Number(value))]);
@@ -15,44 +14,23 @@ var TelegramConfigSchema = z.preprocess((value) => value ?? {}, z.object({
15
14
  apiRoot: z.string().trim().url().default(DEFAULT_TELEGRAM_API_ROOT)
16
15
  }));
17
16
  var StateConfigSchema = z.preprocess((value) => value ?? {}, z.object({ path: z.string().trim().min(1).default(DEFAULT_STATE_FILE_PATH) }));
18
- var OpenRouterConfigSchema = z.preprocess((value) => value ?? {}, z.object({
19
- apiKey: z.string().default(""),
20
- model: z.string().default(DEFAULT_OPENROUTER_MODEL),
21
- timeoutMs: z.coerce.number().int().positive().default(3e4),
22
- transcriptionPrompt: z.string().default("")
23
- }));
24
17
  var AppConfigSchema = z.object({
25
18
  telegram: TelegramConfigSchema,
26
19
  state: StateConfigSchema,
27
- openrouter: OpenRouterConfigSchema,
28
20
  logLevel: z.string().default("info")
29
21
  });
30
22
  function loadAppConfig(configSource = {}, options = {}) {
31
23
  return buildAppConfig(parseConfig(AppConfigSchema, configSource), options);
32
24
  }
33
25
  function buildAppConfig(data, options) {
34
- const openRouterApiKey = normalizeOptionalString$1(data.openrouter.apiKey);
35
- const openRouterModel = normalizeOptionalString$1(data.openrouter.model) ?? "openai/gpt-audio-mini";
36
- const transcriptionPrompt = normalizeOptionalString$1(data.openrouter.transcriptionPrompt);
37
26
  return {
38
27
  telegramBotToken: data.telegram.botToken,
39
28
  telegramAllowedChatIds: data.telegram.allowedChatIds,
40
29
  telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),
41
30
  logLevel: data.logLevel,
42
- stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd()),
43
- openrouter: {
44
- configured: !!openRouterApiKey,
45
- apiKey: openRouterApiKey,
46
- model: openRouterModel,
47
- timeoutMs: data.openrouter.timeoutMs,
48
- transcriptionPrompt
49
- }
31
+ stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd())
50
32
  };
51
33
  }
52
- function normalizeOptionalString$1(value) {
53
- const normalized = value.trim();
54
- return normalized.length > 0 ? normalized : null;
55
- }
56
34
  function resolveStatePath(data, cwd) {
57
35
  return resolve(cwd, data.state.path || "./data/opencode-tbot.state.json");
58
36
  }
@@ -92,8 +70,7 @@ async function preparePluginConfiguration(options) {
92
70
  const globalConfigFilePath = getGlobalPluginConfigFilePath(options.homeDir ?? homedir());
93
71
  const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);
94
72
  const [globalConfig, projectConfig] = await Promise.all([loadPluginConfigFile(globalConfigFilePath), loadPluginConfigFile(projectConfigFilePath)]);
95
- const config = mergePluginConfigSources(globalConfig, projectConfig, options.config);
96
- applyGlobalOpenRouterApiKey(config, globalConfig);
73
+ const config = stripLegacyVoiceConfig(mergePluginConfigSources(globalConfig, projectConfig, options.config));
97
74
  const configFilePath = await pathExists(projectConfigFilePath) ? projectConfigFilePath : globalConfigFilePath;
98
75
  return {
99
76
  cwd: options.cwd,
@@ -123,7 +100,6 @@ function mergePluginConfigSources(...sources) {
123
100
  const normalized = source;
124
101
  const previousTelegram = merged.telegram;
125
102
  const previousState = merged.state;
126
- const previousOpenRouter = merged.openrouter;
127
103
  Object.assign(merged, normalized);
128
104
  if (normalized.telegram) merged.telegram = {
129
105
  ...previousTelegram ?? {},
@@ -133,27 +109,9 @@ function mergePluginConfigSources(...sources) {
133
109
  ...previousState ?? {},
134
110
  ...normalized.state
135
111
  };
136
- if (normalized.openrouter) merged.openrouter = {
137
- ...previousOpenRouter ?? {},
138
- ...normalized.openrouter
139
- };
140
112
  }
141
113
  return merged;
142
114
  }
143
- function applyGlobalOpenRouterApiKey(config, globalConfig) {
144
- const globalApiKey = normalizeOptionalString(globalConfig.openrouter?.apiKey);
145
- if (!config.openrouter) {
146
- if (!globalApiKey) return;
147
- config.openrouter = { apiKey: globalApiKey };
148
- return;
149
- }
150
- if (globalApiKey) {
151
- config.openrouter.apiKey = globalApiKey;
152
- return;
153
- }
154
- delete config.openrouter.apiKey;
155
- if (Object.keys(config.openrouter).length === 0) delete config.openrouter;
156
- }
157
115
  function serializePluginConfig(config) {
158
116
  return `${JSON.stringify(orderPluginConfig(config), null, 2)}\n`;
159
117
  }
@@ -178,13 +136,11 @@ function orderPluginConfig(config) {
178
136
  const prioritizedKeys = new Set([
179
137
  "telegram",
180
138
  "state",
181
- "openrouter",
182
139
  "logLevel"
183
140
  ]);
184
141
  const ordered = {};
185
142
  if (config.telegram) ordered.telegram = config.telegram;
186
143
  if (config.state) ordered.state = config.state;
187
- if (config.openrouter) ordered.openrouter = config.openrouter;
188
144
  if (config.logLevel !== void 0) ordered.logLevel = config.logLevel;
189
145
  for (const [key, value] of Object.entries(config)) if (!prioritizedKeys.has(key)) ordered[key] = value;
190
146
  return ordered;
@@ -192,10 +148,6 @@ function orderPluginConfig(config) {
192
148
  function isPlainObject(value) {
193
149
  return value !== null && typeof value === "object" && !Array.isArray(value);
194
150
  }
195
- function normalizeOptionalString(value) {
196
- const normalized = value?.trim();
197
- return normalized ? normalized : null;
198
- }
199
151
  function isMissingFileError(error) {
200
152
  return error instanceof Error && "code" in error && error.code === "ENOENT";
201
153
  }
@@ -211,7 +163,11 @@ async function pathExists(filePath) {
211
163
  throw error;
212
164
  }
213
165
  }
166
+ function stripLegacyVoiceConfig(config) {
167
+ const { openrouter: _openrouter, ...rest } = config;
168
+ return rest;
169
+ }
214
170
  //#endregion
215
171
  export { writePluginConfigFile as a, loadAppConfig as c, preparePluginConfiguration as i, getOpenCodeConfigFilePath as n, OPENCODE_TBOT_VERSION as o, mergePluginConfigSources as r, DEFAULT_TELEGRAM_API_ROOT as s, getGlobalPluginConfigFilePath as t };
216
172
 
217
- //# sourceMappingURL=plugin-config-DA71_jD3.js.map
173
+ //# sourceMappingURL=plugin-config-B8ginwol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-config-B8ginwol.js","names":[],"sources":["../../src/app/config.ts","../../src/app/package-info.ts","../../src/app/plugin-config.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { z } from \"zod\";\n\nexport const DEFAULT_STATE_FILE_PATH = \"./data/opencode-tbot.state.json\";\nexport const DEFAULT_TELEGRAM_API_ROOT = \"https://api.telegram.org\";\n\nconst AllowedChatIdSchema = z.union([\n z.number().int(),\n z.string().regex(/^-?\\d+$/u).transform((value) => Number(value)),\n]);\n\nconst TelegramConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n botToken: z.string().trim().min(1),\n allowedChatIds: z.array(AllowedChatIdSchema).default([]),\n apiRoot: z.string().trim().url().default(DEFAULT_TELEGRAM_API_ROOT),\n }),\n);\n\nconst StateConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n path: z.string().trim().min(1).default(DEFAULT_STATE_FILE_PATH),\n }),\n);\n\nconst AppConfigSchema = z.object({\n telegram: TelegramConfigSchema,\n state: StateConfigSchema,\n logLevel: z.string().default(\"info\"),\n});\n\nexport interface PluginConfigSource {\n telegram?: {\n botToken?: string;\n allowedChatIds?: Array<number | string>;\n apiRoot?: string;\n [key: string]: unknown;\n };\n state?: {\n path?: string;\n [key: string]: unknown;\n };\n logLevel?: string;\n [key: string]: unknown;\n}\n\nexport interface AppConfig {\n telegramBotToken: string;\n telegramAllowedChatIds: number[];\n telegramApiRoot: string;\n logLevel: string;\n stateFilePath: string;\n}\n\nexport interface LoadAppConfigOptions {\n cwd?: string;\n}\n\nexport function loadAppConfig(\n configSource: PluginConfigSource | undefined = {},\n options: LoadAppConfigOptions = {},\n): AppConfig {\n const parsed = parseConfig(AppConfigSchema, configSource);\n\n return buildAppConfig(parsed, options);\n}\n\nexport const loadPluginConfig = loadAppConfig;\n\nfunction buildAppConfig(\n data: z.infer<typeof AppConfigSchema>,\n options: LoadAppConfigOptions,\n): AppConfig {\n return {\n telegramBotToken: data.telegram.botToken,\n telegramAllowedChatIds: data.telegram.allowedChatIds,\n telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),\n logLevel: data.logLevel,\n stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd()),\n };\n}\n\nfunction resolveStatePath(\n data: z.infer<typeof AppConfigSchema>,\n cwd: string,\n): string {\n return resolve(cwd, data.state.path || DEFAULT_STATE_FILE_PATH);\n}\n\nfunction normalizeApiRoot(value: string): string {\n const normalized = value.trim();\n\n return normalized.endsWith(\"/\")\n ? normalized.slice(0, -1)\n : normalized;\n}\n\nfunction parseConfig<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n configSource: PluginConfigSource | undefined,\n): z.infer<TSchema> {\n const parsed = schema.safeParse(configSource ?? {});\n\n if (parsed.success) {\n return parsed.data;\n }\n\n throw new Error(\n `Invalid plugin configuration: ${JSON.stringify(parsed.error.flatten())}`,\n );\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const OPENCODE_TBOT_VERSION = resolvePackageVersion();\n\nfunction resolvePackageVersion(): string {\n let directory = dirname(fileURLToPath(import.meta.url));\n\n while (true) {\n const packageFilePath = join(directory, \"package.json\");\n\n if (existsSync(packageFilePath)) {\n try {\n const parsed = JSON.parse(readFileSync(packageFilePath, \"utf8\")) as {\n version?: unknown;\n };\n\n if (typeof parsed.version === \"string\" && parsed.version.trim().length > 0) {\n return parsed.version;\n }\n } catch {\n // Fall through and continue searching parent directories.\n }\n }\n\n const parentDirectory = dirname(directory);\n\n if (parentDirectory === directory) {\n break;\n }\n\n directory = parentDirectory;\n }\n\n return \"unknown\";\n}\n","import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { PluginConfigSource } from \"./config.js\";\n\nexport const PLUGIN_CONFIG_FILE_NAME = \"tbot.config.json\";\nexport const GLOBAL_PLUGIN_DIRECTORY_NAME = \"opencode-tbot\";\nexport const GLOBAL_PLUGIN_CONFIG_FILE_NAME = \"config.json\";\nexport const OPENCODE_CONFIG_FILE_NAME = \"opencode.json\";\n\nexport interface PreparedPluginConfiguration {\n cwd: string;\n config: PluginConfigSource;\n globalConfigFilePath: string;\n projectConfigFilePath: string;\n configFilePath: string;\n}\n\nexport interface PreparePluginConfigurationOptions {\n cwd: string;\n config?: PluginConfigSource;\n homeDir?: string;\n}\n\nexport async function preparePluginConfiguration(\n options: PreparePluginConfigurationOptions,\n): Promise<PreparedPluginConfiguration> {\n const homeDir = options.homeDir ?? homedir();\n const globalConfigFilePath = getGlobalPluginConfigFilePath(homeDir);\n const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);\n const [globalConfig, projectConfig] = await Promise.all([\n loadPluginConfigFile(globalConfigFilePath),\n loadPluginConfigFile(projectConfigFilePath),\n ]);\n const config = stripLegacyVoiceConfig(mergePluginConfigSources(globalConfig, projectConfig, options.config));\n const configFilePath = await pathExists(projectConfigFilePath)\n ? projectConfigFilePath\n : globalConfigFilePath;\n\n return {\n cwd: options.cwd,\n config,\n globalConfigFilePath,\n projectConfigFilePath,\n configFilePath,\n };\n}\n\nexport function getOpenCodeConfigDirectory(homeDir: string = homedir()): string {\n return join(homeDir, \".config\", \"opencode\");\n}\n\nexport function getOpenCodeConfigFilePath(homeDir: string = homedir()): string {\n return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_FILE_NAME);\n}\n\nexport function getGlobalPluginConfigFilePath(homeDir: string = homedir()): string {\n return join(\n getOpenCodeConfigDirectory(homeDir),\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n );\n}\n\nexport async function writePluginConfigFile(\n configFilePath: string,\n config: PluginConfigSource,\n): Promise<void> {\n await mkdir(dirname(configFilePath), { recursive: true });\n await writeFile(configFilePath, serializePluginConfig(config), \"utf8\");\n}\n\nexport function mergePluginConfigSources(\n ...sources: Array<PluginConfigSource | undefined>\n): PluginConfigSource {\n const merged: PluginConfigSource = {};\n\n for (const source of sources) {\n if (!source) {\n continue;\n }\n\n const normalized = source;\n const previousTelegram = merged.telegram;\n const previousState = merged.state;\n\n Object.assign(merged, normalized);\n\n if (normalized.telegram) {\n merged.telegram = {\n ...(previousTelegram ?? {}),\n ...normalized.telegram,\n };\n }\n\n if (normalized.state) {\n merged.state = {\n ...(previousState ?? {}),\n ...normalized.state,\n };\n }\n }\n\n return merged;\n}\n\nexport function serializePluginConfig(config: PluginConfigSource): string {\n return `${JSON.stringify(orderPluginConfig(config), null, 2)}\\n`;\n}\n\nasync function loadPluginConfigFile(configFilePath: string): Promise<PluginConfigSource> {\n try {\n const content = await readFile(configFilePath, \"utf8\");\n\n return parsePluginConfigText(content, configFilePath);\n } catch (error) {\n if (isMissingFileError(error)) {\n return {};\n }\n\n throw error;\n }\n}\n\nfunction parsePluginConfigText(\n content: string,\n configFilePath: string,\n): PluginConfigSource {\n try {\n const parsed = JSON.parse(content) as unknown;\n\n if (!isPlainObject(parsed)) {\n throw new Error(\"Config root must be a JSON object.\");\n }\n\n return parsed as PluginConfigSource;\n } catch (error) {\n throw new Error(\n [\n `Failed to parse ${configFilePath} as JSON.`,\n error instanceof Error ? error.message : String(error),\n ].join(\" \"),\n );\n }\n}\n\nfunction orderPluginConfig(config: PluginConfigSource): PluginConfigSource {\n const prioritizedKeys = new Set([\n \"telegram\",\n \"state\",\n \"logLevel\",\n ]);\n const ordered: PluginConfigSource = {};\n\n if (config.telegram) {\n ordered.telegram = config.telegram;\n }\n\n if (config.state) {\n ordered.state = config.state;\n }\n\n if (config.logLevel !== undefined) {\n ordered.logLevel = config.logLevel;\n }\n\n for (const [key, value] of Object.entries(config)) {\n if (!prioritizedKeys.has(key)) {\n ordered[key] = value;\n }\n }\n\n return ordered;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\nasync function resolveProjectPluginConfigFilePath(cwd: string): Promise<string> {\n const preferredPath = join(cwd, PLUGIN_CONFIG_FILE_NAME);\n\n return preferredPath;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch (error) {\n if (isMissingFileError(error)) {\n return false;\n }\n\n throw error;\n }\n}\n\nfunction stripLegacyVoiceConfig(config: PluginConfigSource): PluginConfigSource {\n const { openrouter: _openrouter, ...rest } = config as PluginConfigSource & {\n openrouter?: unknown;\n };\n\n return rest;\n}\n"],"mappings":"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"}