@leviyuan/lodestar 0.1.0 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,16 @@
1
+ [English](docs/README-en.md) | 中文
2
+
1
3
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/leviyuan/lodestar/main/promo.jpg" alt="夜航星 Lodestar" width="100%">
4
+ <img src="promo-cn.jpg" alt="夜航星 Lodestar" width="100%">
3
5
  </p>
4
6
 
5
- # 夜航星 (Lodestar)
7
+ # 夜航星 (Lodestar) v2
8
+
9
+ > **DeepSeek TUI 迁移版**
10
+ >
11
+ > 夜航星已从 Claude Code 迁移至 DeepSeek TUI。核心交互逻辑由 Runtime API + SSE 事件流替代了原有的 tmux spawn + JSONL 文件 IPC。飞书群的协作体验保持不变。
6
12
 
7
- **在你最熟悉的飞书群里,开一段不熄灯的 Claude Code 会话。**
13
+ 飞书 (Lark) 协作通道,用于 [DeepSeek TUI](https://github.com/deepseek-ai/deepseek-tui) — 在飞书群里和 DeepSeek 协作,会话持久化、多项目并行、7×24 可用。
8
14
 
9
15
  ## 项目哲学
10
16
 
@@ -12,93 +18,96 @@ AI 不是帮手,是倍率。它放大的不是体力,是你——你的直
12
18
 
13
19
  夜航星让这件事真正发生:在你思考的地方接住想法,在你转身之后继续把它推向终点。一个群,一个项目,一段不熄灯的对话。你醒着它在听,你睡了它还在跑。
14
20
 
15
- ## 怎么用
16
-
17
- 每个飞书群对应一个 Claude 会话。**群名 = `~/` 下的项目目录名**。
18
-
19
- - 在群里发任意文字 — Claude 接管这一轮,回复以**流式打字机**实时渲染在一张飞书卡片里。
20
- - 思考过程、每一次工具调用都在卡片里被收纳为**可展开折叠面板**:折起来是概述,展开是详情。你随时能审阅它在做什么。
21
- - 需要授权的操作(执行命令、修改文件……)会单独弹一张橙色**权限卡片**,你在群里点 `允许` / `始终允许` / `拒绝` 就行。
22
- - **图片、文件双向互传**:用户发到群里的图/文件,Claude 通过消息里的 `[file: /abs/path]` 提示就能读;Claude 想把文件发回来,在回复任意位置写 `[[send: /abs/path]]`,标记会被剥离,文件以独立消息出现在群里。出站路径限制在该会话的工作目录、`/tmp/lodestar-*` 与 inbox 之内,`/etc`、`~/.ssh`、`~/.config` 等敏感目录被白名单拒绝。
23
- - 一轮跑完,卡片合上、可转发;下一句话开新一轮。
24
-
25
- ### 文本控制指令
26
-
27
- 直接发这四个**裸词**(不需要斜杠,不区分大小写),daemon 拦截、不转发给 Claude:
28
-
29
- | 指令 | 行为 |
30
- | --- | --- |
31
- | `hi` | 未运行时启动;运行中弹一张**控制台卡片**(状态行 + 中断/clear/终止/ls 按钮) |
32
- | `kill` | 优雅关闭 Claude 进程;记住 `sessionId`,下次 `restart` 还能 resume |
33
- | `restart` | 用上一次的 `sessionId` 重启会话(保留上下文) |
34
- | `clear` | 杀掉进程并启动一个全新 session(等价于 Claude Code 的 `/clear`) |
35
-
36
- > 这四个词被全局保留:在群里发 "hi" 当问候也会触发控制台卡片,不会到 Claude 那边。换来的是手机上单手打字的便利。
21
+ ## 核心功能
37
22
 
38
- 整个对话在群里、在手机上、在桌面上完整发生。**离开终端,但不离开 Claude Code。**
23
+ - **Markdown 卡片渲染** — 标题、列表、代码块自动转为飞书富文本卡片
24
+ - **实时反馈** — 👌→✅ 表情追踪消息进度,输出流式转发到群里
25
+ - **权限审批卡片** — 一键批准或拒绝,群里点按钮就能远程授权
26
+ - **图片与文件双向传输** — 截图发给 DeepSeek,DeepSeek 也能传文件回来
27
+ - **会话管理** — 发 `hi` 开工,`restart` / `kill` / `clear` 随时管控
28
+ - **邮件通道** — 受控协作入口:白名单准入、预算上限、权限隔离
29
+ - **状态仪表盘** — 赛博朋克风格监控面板,所有会话一目了然
30
+ - **可靠性保障** — WS 指数退避重连、断线自动恢复、7×24 无人值守
39
31
 
40
32
  ## 安装
41
33
 
42
- ### 1. 准备
43
-
44
- - 一台能常跑后台进程的机器(自家服务器或闲置主机)
45
- - [Bun](https://bun.sh) 运行时
46
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 已登录 Anthropic 账号 (`claude auth login`)
47
- - 一个飞书自建应用 (`cli_xxx`),开通:
48
- - `im:message:send_as_bot` / `im:message` / `im:chat:readonly` / `im:resource`
49
- - `cardkit:card:read` `cardkit:card:write`
50
- `cardkit:card.element:read` `cardkit:card.element:write`
51
- `cardkit:card.settings:read` `cardkit:card.settings:write`
52
-
53
- ### 2. 配置
34
+ ```bash
35
+ npm i -g @leviyuan/lodestar
36
+ lodestar configure # 生成 ~/.deepseek/lodestar.toml
37
+ vi ~/.deepseek/lodestar.toml # 填入飞书凭证
38
+ lodestar daemon # 启动
39
+ ```
54
40
 
55
- 把凭据写到 `~/.config/lodestar/config.toml`:
41
+ 配置文件 `~/.deepseek/lodestar.toml`:
56
42
 
57
43
  ```toml
58
44
  [feishu]
59
- app_id = "cli_xxxxxxxxxxxxxxxx"
60
- app_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
45
+ app_id = "cli_xxxxxxxxxxxxxxxx"
46
+ app_secret = "your_app_secret_here"
61
47
 
62
48
  [runtime]
63
- projects_root = "~/" # 可选,新建群对应的项目目录会落到这里
49
+ port = 7878
50
+ api_token = "your_token_here"
64
51
  ```
65
52
 
66
- 也支持 `LODESTAR_CONFIG=/abs/path.toml` `XDG_CONFIG_HOME` 覆盖。
67
-
68
- ### 3. 启动
53
+ 前置:`deepseek serve --http` 已运行:
69
54
 
70
55
  ```bash
71
- git clone https://github.com/leviyuan/lodestar.git ~/lodestar
72
- cd ~/lodestar
73
- bun install
74
- bun daemon.ts
56
+ DEEPSEEK_RUNTIME_TOKEN=lodestar-runtime-token-v2 deepseek serve --http --port 7878
75
57
  ```
76
58
 
77
- 把机器人拉进任意飞书群,发一条消息——Claude 就上线了。
78
-
79
- > **小贴士**:群名首次出现时,daemon 会自动在 `~/{群名}/` 创建项目目录并 `git init`。换句话说,开新群 = 开新项目。
59
+ ## 指令
80
60
 
81
- ### 4. 守护进程(可选)
61
+ | 指令 | 作用 |
62
+ |------|------|
63
+ | `hi` | 创建或查看会话状态 |
64
+ | `restart` | 重启会话(fork 保留上下文) |
65
+ | `kill` | 关闭会话(archive) |
66
+ | `clear` | 清空上下文(新建 thread) |
67
+ | `ls [path]` | 查看项目目录树 |
68
+ | 普通消息 | 注入为 turn,DeepSeek 处理后回复 |
82
69
 
83
- 要让 daemon 7×24 跑,最简单的方法是配一个 `systemd --user` 单元:
70
+ ## CLI 命令
84
71
 
85
- ```ini
86
- [Unit]
87
- Description=Lodestar daemon
88
- After=network-online.target
72
+ | 命令 | 作用 |
73
+ |------|------|
74
+ | `lodestar daemon` | 启动守护进程 |
75
+ | `lodestar mcp` | 单独启动 MCP server(调试用) |
76
+ | `lodestar configure` | 生成配置文件 |
77
+ | `lodestar status` | 查看运行状态 |
89
78
 
90
- [Service]
91
- Type=simple
92
- ExecStart=/home/USER/.bun/bin/bun /home/USER/lodestar/daemon.ts
93
- Restart=always
94
- RestartSec=3
79
+ ## 架构
95
80
 
96
- [Install]
97
- WantedBy=default.target
98
81
  ```
82
+ 飞书服务器
83
+ │ WebSocket
84
+
85
+ daemon.ts (消息路由进程)
86
+
87
+ ├── Lark WS Client (指数退避重连)
88
+ ├── RuntimeApiClient ────HTTP──┐
89
+ ├── MessageQueue │
90
+ ├── SseEventHandler ←──SSE────┤
91
+ │ │
92
+ │ hi → POST /v1/threads │
93
+ │ restart → fork + archive │
94
+ │ kill → archive │
95
+ │ clear → archive + new │
96
+ └─────────────────────────────┼── localhost:7878
97
+
98
+ ┌──────────────────────────┐
99
+ │ deepseek serve --http │
100
+ │ Runtime API │
101
+ │ │
102
+ │ 加载 feishu MCP server │
103
+ └──────────────────────────┘
104
+ ```
105
+
106
+ ## 仓库
99
107
 
100
- `systemctl --user enable --now lodestar`。
108
+ - **GitHub**: https://github.com/leviyuan/lodestar
109
+ - **npm**: https://www.npmjs.com/package/@leviyuan/lodestar
101
110
 
102
- ## 许可
111
+ ## 许可证
103
112
 
104
113
  [MIT](LICENSE)
package/cli.ts ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * 夜航星 (Lodestar) CLI — npm 全局命令入口
4
+ *
5
+ * 用法:
6
+ * lodestar daemon 启动飞书消息路由守护进程
7
+ * lodestar mcp 启动 Feishu MCP Server (供 DeepSeek TUI 加载)
8
+ * lodestar configure 生成默认配置文件 ~/.deepseek/lodestar/.env
9
+ * lodestar status 查看运行状态
10
+ * lodestar --help 显示帮助
11
+ *
12
+ * npm 全局安装后: npm i -g lodestar
13
+ */
14
+
15
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, chmodSync } from 'fs'
16
+ import { homedir } from 'os'
17
+ import { join } from 'path'
18
+
19
+ const STATE_DIR = join(homedir(), '.deepseek', 'lodestar')
20
+ const CONFIG_FILE = join(homedir(), '.deepseek', 'lodestar.toml')
21
+
22
+ function printHelp(): void {
23
+ console.log(`
24
+ 夜航星 (Lodestar) v2.0.0 — DeepSeek TUI 飞书群协作通道
25
+
26
+ 用法: lodestar <command>
27
+
28
+ 命令:
29
+ daemon 启动飞书消息路由守护进程
30
+ mcp 启动 Feishu MCP Server (供 deepseek serve 加载)
31
+ configure 生成/更新配置文件 ~/.deepseek/lodestar.toml
32
+ status 查看运行状态
33
+
34
+ 示例:
35
+ lodestar configure # 首次安装后运行
36
+ lodestar daemon # 启动守护进程
37
+ lodestar mcp # 单独启动 MCP server (调试用)
38
+
39
+ 配置:
40
+ 飞书凭证配置在 ~/.deepseek/lodestar/.env
41
+ 格式: FEISHU_APP_ID=xxx\\nFEISHU_APP_SECRET=xxx
42
+ `.trim())
43
+ }
44
+
45
+ async function cmdDaemon(): Promise<void> {
46
+ console.log('启动夜航星守护进程...')
47
+ try {
48
+ await import('./daemon.ts')
49
+ } catch (err) {
50
+ console.error('daemon 启动失败:', err)
51
+ process.exit(1)
52
+ }
53
+ }
54
+
55
+ async function cmdMcp(): Promise<void> {
56
+ console.log('启动 Feishu MCP Server...')
57
+ try {
58
+ await import('./feishu-mcp.ts')
59
+ } catch (err) {
60
+ console.error('MCP server 启动失败:', err)
61
+ process.exit(1)
62
+ }
63
+ }
64
+
65
+ function cmdConfigure(): void {
66
+ mkdirSync(STATE_DIR, { recursive: true })
67
+
68
+ if (!existsSync(CONFIG_FILE)) {
69
+ const template = [
70
+ '# 夜航星 (Lodestar) 配置文件',
71
+ '# 从飞书开发者后台获取凭证: https://open.feishu.cn/app',
72
+ '',
73
+ '[feishu]',
74
+ 'app_id = "cli_xxxxxxxxxxxxxxxx"',
75
+ 'app_secret = "your_app_secret_here"',
76
+ '',
77
+ '[projects]',
78
+ '# 项目根目录 (默认 ~/)',
79
+ 'root = "~/"',
80
+ '',
81
+ '[runtime]',
82
+ '# DeepSeek Runtime API 端口 (deepseek serve --http --port)',
83
+ 'port = 7878',
84
+ 'api_token = "your_token_here"',
85
+ '',
86
+ ].join('\n')
87
+ writeFileSync(CONFIG_FILE, template)
88
+ chmodSync(CONFIG_FILE, 0o600)
89
+ console.log(`✅ 配置文件已生成: ${CONFIG_FILE}`)
90
+ console.log(' 请编辑此文件填入你的飞书 App ID 和 Secret')
91
+ } else {
92
+ console.log(`配置文件已存在: ${CONFIG_FILE}`)
93
+ }
94
+
95
+ // Also ensure other state dirs
96
+ mkdirSync(join(STATE_DIR, 'inbox'), { recursive: true })
97
+ mkdirSync(join(STATE_DIR, 'messages'), { recursive: true })
98
+ }
99
+
100
+ function cmdStatus(): void {
101
+ console.log('夜航星 运行状态')
102
+ console.log('────────────────')
103
+
104
+ // Check daemon
105
+ const pidFile = join(STATE_DIR, 'daemon.pid')
106
+ if (existsSync(pidFile)) {
107
+ try {
108
+ const pid = parseInt(readFileSync(pidFile, 'utf8').trim())
109
+ process.kill(pid, 0) // check if alive
110
+ console.log(`🟢 daemon 运行中 (PID ${pid})`)
111
+ } catch {
112
+ console.log('🔴 daemon 已停止 (stale PID)')
113
+ }
114
+ } else {
115
+ console.log('⚪ daemon 未启动')
116
+ }
117
+
118
+ // Check config
119
+ if (existsSync(CONFIG_FILE)) {
120
+ const content = readFileSync(CONFIG_FILE, 'utf8')
121
+ const hasId = content.includes('app_id = "cli_') && !content.includes('cli_xxxxxxxxxxxxxxxx')
122
+ const hasSecret = content.includes('app_secret = "') && !content.includes('your_app_secret_here')
123
+ console.log(hasId && hasSecret ? '🟢 凭证 已配置' : '🟡 凭证 未配置 (运行 lodestar configure 查看)')
124
+ } else {
125
+ console.log('🔴 凭证 配置文件不存在 (运行 lodestar configure)')
126
+ }
127
+
128
+ // Check serve
129
+ try {
130
+ const resp = require('child_process').execSync('curl -sf http://localhost:7878/health 2>/dev/null', { timeout: 3000 }).toString()
131
+ if (resp.includes('ok')) console.log('🟢 serve 运行中 (localhost:7878)')
132
+ } catch {
133
+ console.log('⚪ serve 未运行 (启动: deepseek serve --http --port 7878)')
134
+ }
135
+
136
+ // MCP config
137
+ const mcpFile = join(homedir(), '.deepseek', 'mcp.json')
138
+ if (existsSync(mcpFile)) {
139
+ try {
140
+ const mcp = JSON.parse(readFileSync(mcpFile, 'utf8'))
141
+ if (mcp.feishu) console.log('🟢 MCP 已注册')
142
+ else console.log('🟡 MCP 未注册 (启动 daemon 自动注入)')
143
+ } catch {
144
+ console.log('🟡 MCP 配置解析失败')
145
+ }
146
+ } else {
147
+ console.log('⚪ MCP 未配置')
148
+ }
149
+ }
150
+
151
+ // ── Main ──
152
+ const cmd = process.argv[2]
153
+
154
+ switch (cmd) {
155
+ case 'daemon':
156
+ cmdDaemon()
157
+ break
158
+ case 'mcp':
159
+ cmdMcp()
160
+ break
161
+ case 'configure':
162
+ cmdConfigure()
163
+ break
164
+ case 'status':
165
+ cmdStatus()
166
+ break
167
+ case '--help':
168
+ case '-h':
169
+ case undefined:
170
+ printHelp()
171
+ break
172
+ default:
173
+ console.error(`未知命令: ${cmd}`)
174
+ console.error('运行 lodestar --help 查看帮助')
175
+ process.exit(1)
176
+ }
package/config.ts ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 夜航星配置加载器 — 从 ~/.deepseek/lodestar.toml 读取配置
3
+ *
4
+ * TOML 格式:
5
+ * [feishu]
6
+ * app_id = "cli_xxx"
7
+ * app_secret = "xxx"
8
+ *
9
+ * [projects]
10
+ * root = "~/projects" # 可选,默认 ~/
11
+ *
12
+ * [runtime]
13
+ * port = 7878
14
+ * api_token = "your_token_here"
15
+ *
16
+ * [email] # 可选
17
+ */
18
+
19
+ import { readFileSync, existsSync } from 'fs'
20
+ import { homedir } from 'os'
21
+ import { join } from 'path'
22
+
23
+ export interface LodestarConfig {
24
+ feishu: {
25
+ app_id: string
26
+ app_secret: string
27
+ }
28
+ projects: {
29
+ root: string
30
+ }
31
+ runtime: {
32
+ port: number
33
+ api_token: string
34
+ }
35
+ email?: {
36
+ imap_host?: string
37
+ imap_port?: number
38
+ imap_user?: string
39
+ imap_pass?: string
40
+ smtp_host?: string
41
+ smtp_port?: number
42
+ smtp_user?: string
43
+ smtp_pass?: string
44
+ }
45
+ }
46
+
47
+ const CONFIG_PATH = join(homedir(), '.deepseek', 'lodestar.toml')
48
+
49
+ function expandTilde(v: string): string {
50
+ return v.replace(/^~(?=\/|$)/, homedir())
51
+ }
52
+
53
+ let cached: LodestarConfig | null = null
54
+
55
+ export function loadConfig(): LodestarConfig {
56
+ if (cached) return cached
57
+
58
+ const defaults: LodestarConfig = {
59
+ feishu: { app_id: '', app_secret: '' },
60
+ projects: { root: homedir() },
61
+ runtime: {
62
+ port: 7878,
63
+ api_token: 'your_token_here',
64
+ },
65
+ }
66
+
67
+ if (!existsSync(CONFIG_PATH)) return defaults
68
+
69
+ try {
70
+ const raw = readFileSync(CONFIG_PATH, 'utf8')
71
+ const parsed = (Bun as any).TOML.parse(raw) as Record<string, any>
72
+
73
+ const config: LodestarConfig = { ...defaults }
74
+
75
+ if (parsed.feishu) {
76
+ config.feishu = {
77
+ app_id: parsed.feishu.app_id ?? '',
78
+ app_secret: parsed.feishu.app_secret ?? '',
79
+ }
80
+ }
81
+
82
+ if (parsed.projects?.root) {
83
+ config.projects.root = expandTilde(String(parsed.projects.root))
84
+ }
85
+
86
+ if (parsed.runtime) {
87
+ if (parsed.runtime.port) config.runtime.port = Number(parsed.runtime.port)
88
+ if (parsed.runtime.api_token) config.runtime.api_token = String(parsed.runtime.api_token)
89
+ }
90
+
91
+ if (parsed.email) {
92
+ config.email = {
93
+ imap_host: parsed.email.imap_host,
94
+ imap_port: parsed.email.imap_port ? Number(parsed.email.imap_port) : undefined,
95
+ imap_user: parsed.email.imap_user,
96
+ imap_pass: parsed.email.imap_pass,
97
+ smtp_host: parsed.email.smtp_host,
98
+ smtp_port: parsed.email.smtp_port ? Number(parsed.email.smtp_port) : undefined,
99
+ smtp_user: parsed.email.smtp_user,
100
+ smtp_pass: parsed.email.smtp_pass,
101
+ }
102
+ }
103
+
104
+ cached = config
105
+ return config
106
+ } catch (err) {
107
+ console.error('lodestar: failed to parse config:', err)
108
+ return defaults
109
+ }
110
+ }
111
+
112
+ /** Populate process.env from config for backward compat with env-based code. */
113
+ export function populateEnv(): void {
114
+ const cfg = loadConfig()
115
+
116
+ if (cfg.feishu.app_id && !process.env.FEISHU_APP_ID) {
117
+ process.env.FEISHU_APP_ID = cfg.feishu.app_id
118
+ }
119
+ if (cfg.feishu.app_secret && !process.env.FEISHU_APP_SECRET) {
120
+ process.env.FEISHU_APP_SECRET = cfg.feishu.app_secret
121
+ }
122
+ if (cfg.projects.root && !process.env.FEISHU_PROJECTS_ROOT) {
123
+ process.env.FEISHU_PROJECTS_ROOT = cfg.projects.root
124
+ }
125
+ if (cfg.runtime.port && !process.env.DEEPSEEK_API_URL) {
126
+ process.env.DEEPSEEK_API_URL = `http://localhost:${cfg.runtime.port}`
127
+ }
128
+ if (cfg.runtime.api_token && !process.env.DEEPSEEK_API_TOKEN) {
129
+ process.env.DEEPSEEK_API_TOKEN = cfg.runtime.api_token
130
+ }
131
+ }
132
+
133
+ export function getStateDir(): string {
134
+ return join(homedir(), '.deepseek', 'lodestar')
135
+ }