pocket-coding 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- pocket_coding-0.2.2/PKG-INFO +129 -0
- pocket_coding-0.2.2/README.md +108 -0
- pocket_coding-0.2.2/README.zh-CN.md +110 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/__init__.py +1 -1
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/app.py +19 -37
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/bootstrap/feishu.py +15 -105
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/config/__init__.py +16 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/config/store.py +128 -10
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/providers/codex.py +1 -1
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/runtime.py +12 -13
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/app.py +232 -17
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/config/sections.py +6 -1
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/resources.py +45 -6
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/pyproject.toml +1 -1
- pocket_coding-0.2.1/PKG-INFO +0 -231
- pocket_coding-0.2.1/README.md +0 -210
- pocket_coding-0.2.1/README.zh-CN.md +0 -211
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/.gitignore +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/bootstrap/__init__.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/providers/__init__.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/providers/base.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/providers/claude.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/providers/models.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/__init__.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/app.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/cards.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/messenger.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/models.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/runtime.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/stores.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/relay/utils.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/__init__.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/__init__.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/config/__init__.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/config/claude.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/config/controller.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/config/render_base.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/config/render_forms.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/config/types.py +0 -0
- {pocket_coding-0.2.1 → pocket_coding-0.2.2}/poco/tui/menus/root.py +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pocket-coding
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: PoCo: Pocket Coding for Feishu
|
|
5
|
+
Author: PoCo contributors
|
|
6
|
+
Keywords: bot,codex,feishu,lark,tui
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Communications :: Chat
|
|
16
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: lark-oapi==1.5.3
|
|
19
|
+
Requires-Dist: textual<1,>=0.75
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# PoCo
|
|
23
|
+
|
|
24
|
+
[中文说明](README.zh-CN.md)
|
|
25
|
+
|
|
26
|
+
**PoCo** (Pocket-Coding) is a bot runtime that connects different coding agents to workplace chat software.
|
|
27
|
+
|
|
28
|
+
It currently supports Codex, Claude Code, and Feishu bots.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
See [Quick Start](docs/quickstart.md).
|
|
33
|
+
|
|
34
|
+
## How It Works
|
|
35
|
+
|
|
36
|
+
- DM with the bot: management console
|
|
37
|
+
- Group chat with the bot: project workspace
|
|
38
|
+
- Each project group gets its own worker process
|
|
39
|
+
- Each project is configured from the DM console card
|
|
40
|
+
- PoCo uses Feishu long-connection mode, so no public callback URL is required
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
DM console:
|
|
45
|
+
|
|
46
|
+
- send `poco`
|
|
47
|
+
- use the card UI to create a project and manage projects
|
|
48
|
+
|
|
49
|
+
Project group:
|
|
50
|
+
|
|
51
|
+
- just talk to the agent
|
|
52
|
+
- if you want to send an image, send one Feishu post message that contains both the image and the text prompt
|
|
53
|
+
|
|
54
|
+
## Provider
|
|
55
|
+
|
|
56
|
+
- `codex`: fully implemented through `codex app-server`
|
|
57
|
+
- `claude`: implemented through the Claude Code CLI with streamed JSON output
|
|
58
|
+
|
|
59
|
+
Current provider notes:
|
|
60
|
+
|
|
61
|
+
- Codex is the default runtime path
|
|
62
|
+
- Claude currently supports session attach, image input, and streamed replies
|
|
63
|
+
|
|
64
|
+
## TUI
|
|
65
|
+
|
|
66
|
+
The TUI is a two-panel terminal UI:
|
|
67
|
+
|
|
68
|
+
- left panel: logo and runtime summary
|
|
69
|
+
- right panel: the current interaction panel
|
|
70
|
+
- bottom: an input line that is only enabled when a config field needs text input
|
|
71
|
+
|
|
72
|
+
Shortcuts:
|
|
73
|
+
|
|
74
|
+
- `Ctrl+R`: Save and restart
|
|
75
|
+
- `q`: go back one level
|
|
76
|
+
- `↑ / ↓`: move in the main menu or config menu
|
|
77
|
+
- `Enter`: open the selected item
|
|
78
|
+
- `Esc`: go back one level
|
|
79
|
+
|
|
80
|
+
The default home screen is menu-driven:
|
|
81
|
+
|
|
82
|
+
- the right panel shows:
|
|
83
|
+
- `Agent & Model`
|
|
84
|
+
- `Bot (feishu)`
|
|
85
|
+
- `PoCo`
|
|
86
|
+
- `Language`
|
|
87
|
+
- `Quit`
|
|
88
|
+
- use `↑ / ↓` to select an item
|
|
89
|
+
- press `Enter` to open it
|
|
90
|
+
|
|
91
|
+
Top-level items open their config sections directly:
|
|
92
|
+
|
|
93
|
+
- the right panel turns into a selection menu
|
|
94
|
+
- `↑ / ↓` moves between sections or fields
|
|
95
|
+
- `Enter` opens the current selection
|
|
96
|
+
- `Esc` returns to the previous level
|
|
97
|
+
- `Language` lets you switch between English and Chinese
|
|
98
|
+
- `Bot (feishu)` contains Feishu settings such as `App ID` and `App Secret`
|
|
99
|
+
- `Agent & Model` contains `codex` and `claude`
|
|
100
|
+
- `claude` opens a provider menu first
|
|
101
|
+
- choose a provider such as `anthropic`, `minimax`, `deepseek`, or `kimi`
|
|
102
|
+
- then edit that provider's `base_url`, `auth_token`, `model`, and `extra_env`
|
|
103
|
+
- `model` opens another menu; pick a model first, then run `set_as_default`
|
|
104
|
+
- `extra_env` also opens a submenu; manage env entries one by one instead of editing raw JSON
|
|
105
|
+
- `show` opens the current config in a scrollable view
|
|
106
|
+
- the bottom input line is only active while editing a text field
|
|
107
|
+
|
|
108
|
+
## Files
|
|
109
|
+
|
|
110
|
+
- Workspace bindings: `~/.config/poco/workspaces.json`
|
|
111
|
+
- Per-bot config file: `~/.config/poco/bindings/<app_id>/config.json`
|
|
112
|
+
- Per-bot state directory: `~/.local/state/poco/<app_id>/`
|
|
113
|
+
- Legacy default config/state are still under:
|
|
114
|
+
- `~/.config/poco/config.json`
|
|
115
|
+
- `~/.local/state/poco/`
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
Build packages:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv build
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Quick syntax check:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uv run python -m py_compile poco/__init__.py poco/app.py poco/runtime.py poco/relay/app.py poco/config/store.py poco/tui/app.py
|
|
129
|
+
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# PoCo
|
|
2
|
+
|
|
3
|
+
[中文说明](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
**PoCo** (Pocket-Coding) is a bot runtime that connects different coding agents to workplace chat software.
|
|
6
|
+
|
|
7
|
+
It currently supports Codex, Claude Code, and Feishu bots.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
See [Quick Start](docs/quickstart.md).
|
|
12
|
+
|
|
13
|
+
## How It Works
|
|
14
|
+
|
|
15
|
+
- DM with the bot: management console
|
|
16
|
+
- Group chat with the bot: project workspace
|
|
17
|
+
- Each project group gets its own worker process
|
|
18
|
+
- Each project is configured from the DM console card
|
|
19
|
+
- PoCo uses Feishu long-connection mode, so no public callback URL is required
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
DM console:
|
|
24
|
+
|
|
25
|
+
- send `poco`
|
|
26
|
+
- use the card UI to create a project and manage projects
|
|
27
|
+
|
|
28
|
+
Project group:
|
|
29
|
+
|
|
30
|
+
- just talk to the agent
|
|
31
|
+
- if you want to send an image, send one Feishu post message that contains both the image and the text prompt
|
|
32
|
+
|
|
33
|
+
## Provider
|
|
34
|
+
|
|
35
|
+
- `codex`: fully implemented through `codex app-server`
|
|
36
|
+
- `claude`: implemented through the Claude Code CLI with streamed JSON output
|
|
37
|
+
|
|
38
|
+
Current provider notes:
|
|
39
|
+
|
|
40
|
+
- Codex is the default runtime path
|
|
41
|
+
- Claude currently supports session attach, image input, and streamed replies
|
|
42
|
+
|
|
43
|
+
## TUI
|
|
44
|
+
|
|
45
|
+
The TUI is a two-panel terminal UI:
|
|
46
|
+
|
|
47
|
+
- left panel: logo and runtime summary
|
|
48
|
+
- right panel: the current interaction panel
|
|
49
|
+
- bottom: an input line that is only enabled when a config field needs text input
|
|
50
|
+
|
|
51
|
+
Shortcuts:
|
|
52
|
+
|
|
53
|
+
- `Ctrl+R`: Save and restart
|
|
54
|
+
- `q`: go back one level
|
|
55
|
+
- `↑ / ↓`: move in the main menu or config menu
|
|
56
|
+
- `Enter`: open the selected item
|
|
57
|
+
- `Esc`: go back one level
|
|
58
|
+
|
|
59
|
+
The default home screen is menu-driven:
|
|
60
|
+
|
|
61
|
+
- the right panel shows:
|
|
62
|
+
- `Agent & Model`
|
|
63
|
+
- `Bot (feishu)`
|
|
64
|
+
- `PoCo`
|
|
65
|
+
- `Language`
|
|
66
|
+
- `Quit`
|
|
67
|
+
- use `↑ / ↓` to select an item
|
|
68
|
+
- press `Enter` to open it
|
|
69
|
+
|
|
70
|
+
Top-level items open their config sections directly:
|
|
71
|
+
|
|
72
|
+
- the right panel turns into a selection menu
|
|
73
|
+
- `↑ / ↓` moves between sections or fields
|
|
74
|
+
- `Enter` opens the current selection
|
|
75
|
+
- `Esc` returns to the previous level
|
|
76
|
+
- `Language` lets you switch between English and Chinese
|
|
77
|
+
- `Bot (feishu)` contains Feishu settings such as `App ID` and `App Secret`
|
|
78
|
+
- `Agent & Model` contains `codex` and `claude`
|
|
79
|
+
- `claude` opens a provider menu first
|
|
80
|
+
- choose a provider such as `anthropic`, `minimax`, `deepseek`, or `kimi`
|
|
81
|
+
- then edit that provider's `base_url`, `auth_token`, `model`, and `extra_env`
|
|
82
|
+
- `model` opens another menu; pick a model first, then run `set_as_default`
|
|
83
|
+
- `extra_env` also opens a submenu; manage env entries one by one instead of editing raw JSON
|
|
84
|
+
- `show` opens the current config in a scrollable view
|
|
85
|
+
- the bottom input line is only active while editing a text field
|
|
86
|
+
|
|
87
|
+
## Files
|
|
88
|
+
|
|
89
|
+
- Workspace bindings: `~/.config/poco/workspaces.json`
|
|
90
|
+
- Per-bot config file: `~/.config/poco/bindings/<app_id>/config.json`
|
|
91
|
+
- Per-bot state directory: `~/.local/state/poco/<app_id>/`
|
|
92
|
+
- Legacy default config/state are still under:
|
|
93
|
+
- `~/.config/poco/config.json`
|
|
94
|
+
- `~/.local/state/poco/`
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
Build packages:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv build
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Quick syntax check:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv run python -m py_compile poco/__init__.py poco/app.py poco/runtime.py poco/relay/app.py poco/config/store.py poco/tui/app.py
|
|
108
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# PoCo
|
|
2
|
+
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
**PoCo** (Pocket-Coding) 是一个把不同 Coding-Agent 接入办公软件机器人程序。
|
|
6
|
+
|
|
7
|
+
当前已经支持了 Codex, Claude Code 等 Coding-Agent;已经支持了飞书机器人。
|
|
8
|
+
|
|
9
|
+
## 快速开始
|
|
10
|
+
|
|
11
|
+
点击这里[快速开始](docs/quickstart.zh-CN.md)。
|
|
12
|
+
|
|
13
|
+
## 工作方式
|
|
14
|
+
|
|
15
|
+
- 单聊机器人:管理控制台
|
|
16
|
+
- 群聊机器人:项目工作区
|
|
17
|
+
- 每个项目群会启动一个独立的 worker 进程
|
|
18
|
+
- 每个项目都通过 DM 控制台卡片配置
|
|
19
|
+
- PoCo 使用飞书长连接模式,不需要公网回调地址
|
|
20
|
+
|
|
21
|
+
## 使用方式
|
|
22
|
+
|
|
23
|
+
单聊控制台:
|
|
24
|
+
|
|
25
|
+
- 发 `poco`
|
|
26
|
+
- 用卡片完成新建项目和管理项目
|
|
27
|
+
|
|
28
|
+
项目群:
|
|
29
|
+
|
|
30
|
+
- 直接聊天即可
|
|
31
|
+
- 如果要发图片,请发送一条同时包含图片和文字说明的飞书图文消息
|
|
32
|
+
|
|
33
|
+
## Provider
|
|
34
|
+
|
|
35
|
+
- `codex`:已通过 `codex app-server` 完整接通
|
|
36
|
+
- `claude`:已通过 Claude Code CLI 接通,支持流式 JSON 输出
|
|
37
|
+
|
|
38
|
+
当前 provider 能力说明:
|
|
39
|
+
|
|
40
|
+
- Codex 是默认运行路径
|
|
41
|
+
- Claude 当前支持 session attach、图片输入和流式回复
|
|
42
|
+
|
|
43
|
+
## TUI
|
|
44
|
+
|
|
45
|
+
页面:
|
|
46
|
+
|
|
47
|
+
当前 TUI 是一个双栏终端界面:
|
|
48
|
+
|
|
49
|
+
- 左侧:logo 和运行状态摘要
|
|
50
|
+
- 右侧:当前交互面板
|
|
51
|
+
- 底部:一个输入栏,但只有在需要输入配置值时才会启用
|
|
52
|
+
|
|
53
|
+
快捷键:
|
|
54
|
+
|
|
55
|
+
- `Ctrl+R`:保存并重启
|
|
56
|
+
- `q`:返回上一级
|
|
57
|
+
- `↑ / ↓`:在主菜单或 config 菜单中移动
|
|
58
|
+
- `Enter`:进入当前选项
|
|
59
|
+
- `Esc`:返回上一级
|
|
60
|
+
|
|
61
|
+
默认首页是菜单驱动的:
|
|
62
|
+
|
|
63
|
+
- 右侧会显示:
|
|
64
|
+
- `Agent & Model`
|
|
65
|
+
- `Bot (feishu)`
|
|
66
|
+
- `PoCo`
|
|
67
|
+
- `Language`
|
|
68
|
+
- `Quit`
|
|
69
|
+
- 用 `↑ / ↓` 选择项目
|
|
70
|
+
- 按 `Enter` 进入
|
|
71
|
+
|
|
72
|
+
顶层菜单会直接进入对应配置分类:
|
|
73
|
+
|
|
74
|
+
- 右侧面板会变成选择菜单
|
|
75
|
+
- `↑ / ↓` 可以切换 section 或字段
|
|
76
|
+
- `Enter` 进入当前项
|
|
77
|
+
- `Esc` 返回上一级
|
|
78
|
+
- `Language` 可以切换英文和中文
|
|
79
|
+
- `Bot (feishu)` 里包含 `feishu` 相关设置,比如 `App ID`、`App Secret`
|
|
80
|
+
- `Agent & Model` 里包含 `codex` 和 `claude`
|
|
81
|
+
- `claude` 会先进入 provider 菜单
|
|
82
|
+
- 先选一个 provider,例如 `anthropic`、`minimax`、`deepseek`、`kimi`
|
|
83
|
+
- 再编辑这个 provider 的 `base_url`、`auth_token`、`model`、`extra_env`
|
|
84
|
+
- `model` 会再进入一个模型菜单;先选模型,再执行 `set_as_default`
|
|
85
|
+
- `extra_env` 也会进入子菜单;按项管理环境变量,不再直接编辑原始 JSON
|
|
86
|
+
- `show` 会打开当前配置文件的滚动视图
|
|
87
|
+
- 只有在编辑文本字段时,底部输入栏才会启用
|
|
88
|
+
|
|
89
|
+
## 文件位置
|
|
90
|
+
|
|
91
|
+
- 工作区绑定:`~/.config/poco/workspaces.json`
|
|
92
|
+
- 每个 bot 的配置文件:`~/.config/poco/bindings/<app_id>/config.json`
|
|
93
|
+
- 每个 bot 的状态目录:`~/.local/state/poco/<app_id>/`
|
|
94
|
+
- 兼容旧版时,默认配置 / 状态仍在:
|
|
95
|
+
- `~/.config/poco/config.json`
|
|
96
|
+
- `~/.local/state/poco/`
|
|
97
|
+
|
|
98
|
+
## 开发
|
|
99
|
+
|
|
100
|
+
构建安装包:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv build
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
快速语法检查:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv run python -m py_compile poco/__init__.py poco/app.py poco/runtime.py poco/relay/app.py poco/config/store.py poco/tui/app.py
|
|
110
|
+
```
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import argparse
|
|
3
|
-
import fcntl
|
|
4
3
|
import json
|
|
5
4
|
import logging
|
|
6
5
|
import os
|
|
@@ -12,36 +11,10 @@ from .runtime import (
|
|
|
12
11
|
PoCoService,
|
|
13
12
|
RingLogHandler,
|
|
14
13
|
)
|
|
15
|
-
from .config import
|
|
14
|
+
from .config import ConfigStore, build_paths, ensure_dirs, get_nested, workspace_binding
|
|
16
15
|
from .tui import PoCoTui
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
_APP_LOCK = None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _acquire_app_lock() -> bool:
|
|
23
|
-
"""Acquires a process-wide PoCo instance lock.
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
True when this process successfully acquired the lock, or False when
|
|
27
|
-
another PoCo process is already holding it.
|
|
28
|
-
"""
|
|
29
|
-
global _APP_LOCK
|
|
30
|
-
lock_path = Path(STATE_DIR) / "poco.lock"
|
|
31
|
-
handle = lock_path.open("w", encoding="utf-8")
|
|
32
|
-
try:
|
|
33
|
-
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
34
|
-
except BlockingIOError:
|
|
35
|
-
handle.close()
|
|
36
|
-
return False
|
|
37
|
-
handle.seek(0)
|
|
38
|
-
handle.truncate()
|
|
39
|
-
handle.write(str(os.getpid()))
|
|
40
|
-
handle.flush()
|
|
41
|
-
_APP_LOCK = handle
|
|
42
|
-
return True
|
|
43
|
-
|
|
44
|
-
|
|
45
18
|
def parse_args() -> argparse.Namespace:
|
|
46
19
|
parser = argparse.ArgumentParser(description="PoCo")
|
|
47
20
|
parser.add_argument("--log-level", default="INFO")
|
|
@@ -60,15 +33,16 @@ def main() -> None:
|
|
|
60
33
|
level=getattr(logging, str(getattr(args, "log_level", "INFO")).upper(), logging.INFO),
|
|
61
34
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
62
35
|
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
36
|
+
workspace = Path.cwd()
|
|
37
|
+
binding = workspace_binding(workspace)
|
|
38
|
+
paths = build_paths(binding or "default")
|
|
39
|
+
ensure_dirs(paths)
|
|
66
40
|
root_logger = logging.getLogger()
|
|
67
41
|
ring = RingLogHandler()
|
|
68
42
|
ring.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
|
69
43
|
root_logger.addHandler(ring)
|
|
70
44
|
file_handler = RotatingFileHandler(
|
|
71
|
-
|
|
45
|
+
paths.log_path,
|
|
72
46
|
maxBytes=2_000_000,
|
|
73
47
|
backupCount=3,
|
|
74
48
|
encoding="utf-8",
|
|
@@ -76,10 +50,14 @@ def main() -> None:
|
|
|
76
50
|
)
|
|
77
51
|
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
|
78
52
|
root_logger.addHandler(file_handler)
|
|
79
|
-
logging.getLogger("poco").info(
|
|
53
|
+
logging.getLogger("poco").info(
|
|
54
|
+
"Persistent log file enabled at %s (instance=%s)",
|
|
55
|
+
paths.log_path,
|
|
56
|
+
binding or "default",
|
|
57
|
+
)
|
|
80
58
|
|
|
81
|
-
store = ConfigStore(
|
|
82
|
-
service = PoCoService(store, ring)
|
|
59
|
+
store = ConfigStore(paths.config_path, paths)
|
|
60
|
+
service = PoCoService(store, ring, paths)
|
|
83
61
|
|
|
84
62
|
if args.command == "config" and args.show:
|
|
85
63
|
print(json.dumps(service.masked_config(), ensure_ascii=False, indent=2))
|
|
@@ -92,10 +70,14 @@ def main() -> None:
|
|
|
92
70
|
print(json.dumps({path: current}, ensure_ascii=False, indent=2))
|
|
93
71
|
return
|
|
94
72
|
if args.command == "feishu-bootstrap":
|
|
95
|
-
raise SystemExit(run_feishu_bootstrap_cli(
|
|
73
|
+
raise SystemExit(run_feishu_bootstrap_cli(workspace))
|
|
96
74
|
|
|
97
|
-
|
|
75
|
+
skip_bind_once = os.environ.pop("POCO_SKIP_BIND_ONCE", "") == "1"
|
|
76
|
+
app = PoCoTui(service, focus_config=args.command == "config", skip_bind_once=skip_bind_once)
|
|
98
77
|
result = app.run()
|
|
78
|
+
if result == "restart-bound":
|
|
79
|
+
os.environ["POCO_SKIP_BIND_ONCE"] = "1"
|
|
80
|
+
os.execv(sys.executable, [sys.executable, *sys.argv])
|
|
99
81
|
if result == "restart":
|
|
100
82
|
os.execv(sys.executable, [sys.executable, *sys.argv])
|
|
101
83
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import getpass
|
|
6
|
-
import re
|
|
7
6
|
from dataclasses import dataclass, field
|
|
8
|
-
from
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, List, Set
|
|
9
9
|
|
|
10
10
|
import lark_oapi as lark
|
|
11
11
|
import requests
|
|
@@ -15,13 +15,12 @@ from lark_oapi.api.application.v6 import (
|
|
|
15
15
|
Application,
|
|
16
16
|
Callback,
|
|
17
17
|
GetApplicationRequest,
|
|
18
|
-
ListApplicationAppVersionRequest,
|
|
19
18
|
ListScopeRequest,
|
|
20
19
|
PatchApplicationRequest,
|
|
21
20
|
SubscribedEvent,
|
|
22
21
|
)
|
|
23
22
|
|
|
24
|
-
from ..config import ConfigStore, get_nested, normalize_config_key, set_nested
|
|
23
|
+
from ..config import ConfigStore, bind_workspace, build_paths, ensure_dirs, get_nested, normalize_config_key, set_nested, workspace_binding
|
|
25
24
|
|
|
26
25
|
BASE_URL = "https://open.feishu.cn/open-apis"
|
|
27
26
|
REQUIRED_SCOPES = [
|
|
@@ -52,21 +51,9 @@ class CurrentAppState:
|
|
|
52
51
|
callbacks: Set[str] = field(default_factory=set)
|
|
53
52
|
|
|
54
53
|
|
|
55
|
-
@dataclass(slots=True)
|
|
56
|
-
class PublishResult:
|
|
57
|
-
"""Best-effort publish result."""
|
|
58
|
-
|
|
59
|
-
created_version: bool = False
|
|
60
|
-
published: bool = False
|
|
61
|
-
version_tag: str = ""
|
|
62
|
-
warnings: List[str] = field(default_factory=list)
|
|
63
|
-
|
|
64
|
-
|
|
65
54
|
class FeishuBootstrapper:
|
|
66
55
|
"""Applies PoCo's desired configuration to one Feishu app."""
|
|
67
56
|
|
|
68
|
-
_SEMVER_RE = re.compile(r"^\s*(\d+)\.(\d+)\.(\d+)\s*$")
|
|
69
|
-
|
|
70
57
|
def __init__(self, app_id: str, app_secret: str, store: ConfigStore) -> None:
|
|
71
58
|
self.app_id = app_id.strip()
|
|
72
59
|
self.app_secret = app_secret.strip()
|
|
@@ -241,87 +228,6 @@ class FeishuBootstrapper:
|
|
|
241
228
|
f"log_id={response.get_log_id()}"
|
|
242
229
|
)
|
|
243
230
|
|
|
244
|
-
@classmethod
|
|
245
|
-
def _parse_semver(cls, value: str) -> Optional[Tuple[int, int, int]]:
|
|
246
|
-
"""Parses a semantic version string."""
|
|
247
|
-
match = cls._SEMVER_RE.match(value or "")
|
|
248
|
-
if not match:
|
|
249
|
-
return None
|
|
250
|
-
return tuple(int(part) for part in match.groups())
|
|
251
|
-
|
|
252
|
-
@staticmethod
|
|
253
|
-
def _format_semver(version: Tuple[int, int, int]) -> str:
|
|
254
|
-
"""Formats a semantic version tuple."""
|
|
255
|
-
return ".".join(str(part) for part in version)
|
|
256
|
-
|
|
257
|
-
def _next_version_tag(self) -> Tuple[str, List[str]]:
|
|
258
|
-
"""Computes the next app version tag.
|
|
259
|
-
|
|
260
|
-
Returns:
|
|
261
|
-
A tuple of (next_version, warnings).
|
|
262
|
-
"""
|
|
263
|
-
warnings: List[str] = []
|
|
264
|
-
request = (
|
|
265
|
-
ListApplicationAppVersionRequest.builder()
|
|
266
|
-
.app_id(self.app_id)
|
|
267
|
-
.lang("en_us")
|
|
268
|
-
.page_size(50)
|
|
269
|
-
.build()
|
|
270
|
-
)
|
|
271
|
-
response = self._client.application.v6.application_app_version.list(request)
|
|
272
|
-
if response.code != 0:
|
|
273
|
-
raise RuntimeError(
|
|
274
|
-
f"Feishu app version list failed: code={response.code}, msg={response.msg}, "
|
|
275
|
-
f"log_id={response.get_log_id()}"
|
|
276
|
-
)
|
|
277
|
-
items = response.data.items if response.data is not None and response.data.items is not None else []
|
|
278
|
-
if not items:
|
|
279
|
-
return "0.1.0", warnings
|
|
280
|
-
parsed_versions: List[Tuple[int, int, int]] = []
|
|
281
|
-
ignored_versions: List[str] = []
|
|
282
|
-
for item in items:
|
|
283
|
-
version = str(getattr(item, "version", "") or "").strip()
|
|
284
|
-
if not version:
|
|
285
|
-
continue
|
|
286
|
-
parsed = self._parse_semver(version)
|
|
287
|
-
if parsed is None:
|
|
288
|
-
ignored_versions.append(version)
|
|
289
|
-
continue
|
|
290
|
-
parsed_versions.append(parsed)
|
|
291
|
-
if not parsed_versions:
|
|
292
|
-
if ignored_versions:
|
|
293
|
-
warnings.append(
|
|
294
|
-
"Existing app versions are not semantic versions and were ignored: "
|
|
295
|
-
+ ", ".join(sorted(set(ignored_versions))[:5])
|
|
296
|
-
)
|
|
297
|
-
return "0.1.0", warnings
|
|
298
|
-
major, minor, patch = max(parsed_versions)
|
|
299
|
-
next_version = (major, minor, patch + 1)
|
|
300
|
-
return self._format_semver(next_version), warnings
|
|
301
|
-
|
|
302
|
-
def create_and_publish_version(self, tenant_access_token: str) -> PublishResult:
|
|
303
|
-
"""Best-effort version guidance.
|
|
304
|
-
|
|
305
|
-
Feishu's application patch and scope flows are scriptable, but version
|
|
306
|
-
creation / publish is still unreliable through the current API path
|
|
307
|
-
used by PoCo. Instead of failing bootstrap, PoCo computes the next
|
|
308
|
-
semantic version and tells the user what to publish manually.
|
|
309
|
-
"""
|
|
310
|
-
result = PublishResult()
|
|
311
|
-
try:
|
|
312
|
-
version_tag, version_warnings = self._next_version_tag()
|
|
313
|
-
result.version_tag = version_tag
|
|
314
|
-
result.warnings.extend(version_warnings)
|
|
315
|
-
except Exception as exc:
|
|
316
|
-
result.warnings.append(f"Version lookup failed, fallback to 0.1.0: {exc}")
|
|
317
|
-
version_tag = "0.1.0"
|
|
318
|
-
result.version_tag = version_tag
|
|
319
|
-
result.warnings.append(
|
|
320
|
-
"App config is ready, but Feishu version creation / publish still needs to be done "
|
|
321
|
-
f"manually in Open Platform. Suggested next version: {version_tag}."
|
|
322
|
-
)
|
|
323
|
-
return result
|
|
324
|
-
|
|
325
231
|
|
|
326
232
|
def _prompt_with_default(prompt: str, default: str, *, secret: bool = False) -> str:
|
|
327
233
|
"""Prompts for one value, keeping the existing default on empty input."""
|
|
@@ -332,9 +238,11 @@ def _prompt_with_default(prompt: str, default: str, *, secret: bool = False) ->
|
|
|
332
238
|
return value or default
|
|
333
239
|
|
|
334
240
|
|
|
335
|
-
def run_feishu_bootstrap_cli(
|
|
241
|
+
def run_feishu_bootstrap_cli(workspace: Path) -> int:
|
|
336
242
|
"""Runs Feishu bootstrap as a simple two-step CLI wizard."""
|
|
337
|
-
|
|
243
|
+
bound_app_id = workspace_binding(workspace)
|
|
244
|
+
draft_store = ConfigStore(build_paths(bound_app_id or "default").config_path, build_paths(bound_app_id or "default"))
|
|
245
|
+
config = draft_store.load()
|
|
338
246
|
draft_app_id = str(get_nested(config, "feishu.app_id") or "").strip()
|
|
339
247
|
draft_app_secret = str(get_nested(config, "feishu.app_secret") or "").strip()
|
|
340
248
|
|
|
@@ -353,16 +261,21 @@ def run_feishu_bootstrap_cli(store: ConfigStore) -> int:
|
|
|
353
261
|
print("ERROR APP Secret is required.")
|
|
354
262
|
return 1
|
|
355
263
|
|
|
264
|
+
bound_paths = build_paths(app_id)
|
|
265
|
+
ensure_dirs(bound_paths)
|
|
266
|
+
store = ConfigStore(bound_paths.config_path, bound_paths)
|
|
356
267
|
bootstrapper = FeishuBootstrapper(app_id, app_secret, store)
|
|
357
268
|
try:
|
|
358
269
|
print("INFO Starting Feishu bootstrap.")
|
|
359
270
|
print("INFO Validating APP ID / APP Secret.")
|
|
360
|
-
|
|
271
|
+
bootstrapper.validate_credentials()
|
|
361
272
|
print("OK Credentials are valid.")
|
|
362
273
|
|
|
363
274
|
print("INFO Saving local PoCo config.")
|
|
364
275
|
bootstrapper.save_local_credentials()
|
|
365
|
-
|
|
276
|
+
bind_workspace(workspace, app_id)
|
|
277
|
+
print(f"OK Bound workspace `{workspace}` to `{app_id}`.")
|
|
278
|
+
print(f"OK Saved APP ID / APP Secret into {bound_paths.config_path}.")
|
|
366
279
|
|
|
367
280
|
print("INFO Reading current Feishu app state.")
|
|
368
281
|
current = bootstrapper.fetch_current_state()
|
|
@@ -381,10 +294,7 @@ def run_feishu_bootstrap_cli(store: ConfigStore) -> int:
|
|
|
381
294
|
bootstrapper.apply_scopes()
|
|
382
295
|
print("OK Scope application submitted.")
|
|
383
296
|
|
|
384
|
-
print("INFO
|
|
385
|
-
publish_result = bootstrapper.create_and_publish_version(tenant_token)
|
|
386
|
-
for warning in publish_result.warnings:
|
|
387
|
-
print(f"INFO {warning}")
|
|
297
|
+
print("INFO Bootstrap completed. Create and publish the new app version manually in Feishu Open Platform.")
|
|
388
298
|
|
|
389
299
|
print("FINISH PoCo bootstrap finished.")
|
|
390
300
|
return 0
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from .store import (
|
|
2
|
+
PoCoPaths,
|
|
3
|
+
WORKSPACE_BINDINGS_PATH,
|
|
2
4
|
CONFIG_KEY_ALIASES,
|
|
3
5
|
CONFIG_PATH,
|
|
4
6
|
DEFAULT_CONFIG,
|
|
@@ -8,17 +10,25 @@ from .store import (
|
|
|
8
10
|
THREAD_STATE_PATH,
|
|
9
11
|
WORKER_STATE_PATH,
|
|
10
12
|
ConfigStore,
|
|
13
|
+
build_paths,
|
|
14
|
+
bind_workspace,
|
|
11
15
|
config_ready,
|
|
12
16
|
ensure_dirs,
|
|
13
17
|
get_nested,
|
|
18
|
+
load_workspace_bindings,
|
|
14
19
|
mask_secret,
|
|
15
20
|
missing_required_config_paths,
|
|
21
|
+
normalize_instance_name,
|
|
16
22
|
normalize_config_key,
|
|
17
23
|
parse_config_value,
|
|
24
|
+
saved_feishu_bot_ids,
|
|
18
25
|
set_nested,
|
|
26
|
+
workspace_binding,
|
|
19
27
|
)
|
|
20
28
|
|
|
21
29
|
__all__ = [
|
|
30
|
+
"PoCoPaths",
|
|
31
|
+
"WORKSPACE_BINDINGS_PATH",
|
|
22
32
|
"CONFIG_KEY_ALIASES",
|
|
23
33
|
"CONFIG_PATH",
|
|
24
34
|
"DEFAULT_CONFIG",
|
|
@@ -28,12 +38,18 @@ __all__ = [
|
|
|
28
38
|
"THREAD_STATE_PATH",
|
|
29
39
|
"WORKER_STATE_PATH",
|
|
30
40
|
"ConfigStore",
|
|
41
|
+
"build_paths",
|
|
42
|
+
"bind_workspace",
|
|
31
43
|
"config_ready",
|
|
32
44
|
"ensure_dirs",
|
|
33
45
|
"get_nested",
|
|
46
|
+
"load_workspace_bindings",
|
|
34
47
|
"mask_secret",
|
|
35
48
|
"missing_required_config_paths",
|
|
49
|
+
"normalize_instance_name",
|
|
36
50
|
"normalize_config_key",
|
|
37
51
|
"parse_config_value",
|
|
52
|
+
"saved_feishu_bot_ids",
|
|
38
53
|
"set_nested",
|
|
54
|
+
"workspace_binding",
|
|
39
55
|
]
|