pycodex-ws 0.5.2__tar.gz → 0.6.0__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.
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/PKG-INFO +29 -4
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/README.md +27 -2
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/server.py +15 -15
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_ws/__init__.py +2 -5
- pycodex_ws-0.6.0/pycodex_ws/feishu_auth.py +30 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_ws/feishu_card.py +18 -243
- pycodex_ws-0.5.2/pycodex_ws/feishu_link.py → pycodex_ws-0.6.0/pycodex_ws/feishu_connection.py +15 -166
- pycodex_ws-0.6.0/pycodex_ws/feishu_link.py +157 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_ws/feishu_oauth.py +3 -4
- pycodex_ws-0.6.0/pycodex_ws/feishu_render.py +215 -0
- pycodex_ws-0.6.0/pycodex_ws/server_support.py +299 -0
- pycodex_ws-0.6.0/pycodex_ws/workspace_config.py +186 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pyproject.toml +2 -2
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/WORKSPACE_API.md +71 -13
- pycodex_ws-0.6.0/workspace_server/app.py +438 -0
- pycodex_ws-0.6.0/workspace_server/content_views.css +1 -0
- pycodex_ws-0.6.0/workspace_server/content_views.mjs +37 -0
- pycodex_ws-0.6.0/workspace_server/frontend/content_views.css +176 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/content_views.mjs +69 -6
- pycodex_ws-0.6.0/workspace_server/frontend/transcript.mjs +385 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/README.md +4 -1
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/README.md +107 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview-mobile.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.html +43 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.css +67 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.js +102 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/fixture.mjs +85 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/verify.mjs +82 -0
- pycodex_ws-0.6.0/workspace_server/routes.py +694 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/runtime_info.py +3 -1
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/session_api.py +2 -1
- pycodex_ws-0.6.0/workspace_server/session_view.py +669 -0
- pycodex_ws-0.6.0/workspace_server/sessions.py +435 -0
- pycodex_ws-0.6.0/workspace_server/view_sources.py +251 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspace.html +268 -70
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspace_views.py +5 -248
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspaces.py +15 -198
- pycodex_ws-0.5.2/workspace_server/app.py +0 -2519
- pycodex_ws-0.5.2/workspace_server/content_views.css +0 -1
- pycodex_ws-0.5.2/workspace_server/content_views.mjs +0 -32
- pycodex_ws-0.5.2/workspace_server/frontend/content_views.css +0 -113
- pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/README.md +0 -87
- pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/preview-mobile.png +0 -0
- pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/preview.html +0 -33
- pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/preview.png +0 -0
- pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/source/custom.css +0 -8
- pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/source/custom.js +0 -92
- pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status/source/fixture.mjs +0 -22
- pycodex_ws-0.5.2/workspace_server/optional_features/tool-calls/source/verify.mjs +0 -80
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/.gitignore +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/LICENSE +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/__init__.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/__main__.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/browser.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/client.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/index.html +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/pycodex_router/protocol.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/__init__.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/__main__.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/agent_access.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/content_views.LICENSE.txt +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/customization.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/editor.css +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/editor.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/.gitignore +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/build.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/composer.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.css +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/editor_context.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/package-lock.json +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/package.json +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/prompt_history.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/touch_selection.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/frontend/view_labels.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/CONTRIBUTING.md +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/README.md +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/preview-mobile.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/preview.html +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/preview.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/fixture.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.css +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.js +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/consecutive-tool-groups/source/verify.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/README.md +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/preview-mobile.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/preview.html +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/preview.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/custom.css +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/custom.js +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/fixture.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/exec-status/source/verify.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/README.md +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/preview-mobile.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/preview.html +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/preview.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/custom.css +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/custom.js +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/fixture.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/reasoning-display/source/verify.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/README.md +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/preview-mobile.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/preview.html +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/preview.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/custom.css +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/custom.js +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/fixture.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/session-tips.js +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/session-tips/source/verify.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/README.md +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/preview-mobile.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/preview.html +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/preview.png +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/source/custom.css +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/source/custom.js +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/optional_features/tool-calls/source/fixture.mjs +0 -0
- {pycodex_ws-0.5.2/workspace_server/optional_features/subagent-status → pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls}/source/verify.mjs +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/owned_process.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/sharing.py +0 -0
- {pycodex_ws-0.5.2 → pycodex_ws-0.6.0}/workspace_server/workspaces.html +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pycodex-ws
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Browser workspace and optional Feishu integration for python-codex
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.7
|
|
7
7
|
Requires-Dist: pillow>=9.0.0
|
|
8
|
-
Requires-Dist: python-codex==0.
|
|
8
|
+
Requires-Dist: python-codex==0.6.0
|
|
9
9
|
Requires-Dist: websockets<16,>=11
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
|
|
@@ -16,6 +16,9 @@ optional Feishu integration for
|
|
|
16
16
|
[`python-codex`](https://pypi.org/project/python-codex/).
|
|
17
17
|
Requires Python 3.7+ and installs the matching core version.
|
|
18
18
|
|
|
19
|
+
See the [0.6.0 release notes](https://github.com/Randomizez/pycodex/blob/v0.6.0/docs/RELEASE_0.6.0.md) for changes and
|
|
20
|
+
integration migration from 0.5.0.
|
|
21
|
+
|
|
19
22
|
## Quick start
|
|
20
23
|
|
|
21
24
|
```bash
|
|
@@ -68,6 +71,15 @@ When using a router, its directory groups workspaces by device and lists unread
|
|
|
68
71
|
workspaces at the top. Device handles reorder the list; pencil buttons edit
|
|
69
72
|
device names.
|
|
70
73
|
|
|
74
|
+
The runtime information shared between Agents recommends plain messages, which
|
|
75
|
+
steer active work at the next safe boundary. Use `/queue` explicitly for an
|
|
76
|
+
independent follow-up task that should wait for current work to finish.
|
|
77
|
+
|
|
78
|
+
The message API also accepts a string `name`, defaulting to `"user"`, for
|
|
79
|
+
identifying the source of a user message. The history API returns it, and
|
|
80
|
+
recorded sessions preserve it across resume. It is metadata only: the model request and frontend display do
|
|
81
|
+
not include it. See the [message API contract](workspace_server/WORKSPACE_API.md#commands-and-results).
|
|
82
|
+
|
|
71
83
|
The [VS Code extension](extensions/vscode/README.md) uses this backend and is
|
|
72
84
|
distributed separately as a VSIX.
|
|
73
85
|
|
|
@@ -90,6 +102,11 @@ state, model catalogs, asynchronous command results, paginated history, event
|
|
|
90
102
|
replay and sub-agent/background-work inventories. A `mount(host)` export in
|
|
91
103
|
`custom.js` can access the built-in page's session, draft and view controls.
|
|
92
104
|
|
|
105
|
+
Backend module ownership and the preserved session interfaces are documented in
|
|
106
|
+
the [runtime guide](../../docs/RUNTIME.md#workspace-session-api).
|
|
107
|
+
File/Git source resolution and view storage retain their existing public classes;
|
|
108
|
+
see [content views](../../docs/RUNTIME.md#workspace-content-views).
|
|
109
|
+
|
|
93
110
|
Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
|
|
94
111
|
in the view area, then choose features by name or ID in chat. The Agent adapts
|
|
95
112
|
their source into your frontend. To share a local change, ask it to publish a
|
|
@@ -144,6 +161,11 @@ token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
|
|
|
144
161
|
`/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
|
|
145
162
|
Other workspace features work without the SDK.
|
|
146
163
|
|
|
164
|
+
The adapter separates card rendering, refresh-token storage and SDK connections.
|
|
165
|
+
`PycodexCard` and `PycodexRuntimeLink` keep their existing imports and interfaces;
|
|
166
|
+
see [connection lifecycle](../../docs/RUNTIME.md#feishu-connection-lifecycle)
|
|
167
|
+
and [event presentation](../../docs/RUNTIME.md#observation).
|
|
168
|
+
|
|
147
169
|
## Development
|
|
148
170
|
|
|
149
171
|
From the repository root:
|
|
@@ -155,6 +177,9 @@ uv run --dev pytest packages/pycodex-ws/tests
|
|
|
155
177
|
uv build packages/pycodex-ws
|
|
156
178
|
```
|
|
157
179
|
|
|
158
|
-
Python integrations
|
|
159
|
-
|
|
180
|
+
Python integrations construct a runtime through `AgentsManager.create_root_runtime()`
|
|
181
|
+
and call `pycodex_ws.register_feishu_commands(runtime)` to include `/link` and
|
|
182
|
+
`/unlink`. The context indicator shows tokens against the auto-compaction limit;
|
|
183
|
+
its expandable Agent tree shows each task's status and latest assistant text.
|
|
184
|
+
See [runtime contracts](../../docs/RUNTIME.md) and
|
|
160
185
|
[test documentation](../../tests/TESTS.md).
|
|
@@ -5,6 +5,9 @@ optional Feishu integration for
|
|
|
5
5
|
[`python-codex`](https://pypi.org/project/python-codex/).
|
|
6
6
|
Requires Python 3.7+ and installs the matching core version.
|
|
7
7
|
|
|
8
|
+
See the [0.6.0 release notes](https://github.com/Randomizez/pycodex/blob/v0.6.0/docs/RELEASE_0.6.0.md) for changes and
|
|
9
|
+
integration migration from 0.5.0.
|
|
10
|
+
|
|
8
11
|
## Quick start
|
|
9
12
|
|
|
10
13
|
```bash
|
|
@@ -57,6 +60,15 @@ When using a router, its directory groups workspaces by device and lists unread
|
|
|
57
60
|
workspaces at the top. Device handles reorder the list; pencil buttons edit
|
|
58
61
|
device names.
|
|
59
62
|
|
|
63
|
+
The runtime information shared between Agents recommends plain messages, which
|
|
64
|
+
steer active work at the next safe boundary. Use `/queue` explicitly for an
|
|
65
|
+
independent follow-up task that should wait for current work to finish.
|
|
66
|
+
|
|
67
|
+
The message API also accepts a string `name`, defaulting to `"user"`, for
|
|
68
|
+
identifying the source of a user message. The history API returns it, and
|
|
69
|
+
recorded sessions preserve it across resume. It is metadata only: the model request and frontend display do
|
|
70
|
+
not include it. See the [message API contract](workspace_server/WORKSPACE_API.md#commands-and-results).
|
|
71
|
+
|
|
60
72
|
The [VS Code extension](extensions/vscode/README.md) uses this backend and is
|
|
61
73
|
distributed separately as a VSIX.
|
|
62
74
|
|
|
@@ -79,6 +91,11 @@ state, model catalogs, asynchronous command results, paginated history, event
|
|
|
79
91
|
replay and sub-agent/background-work inventories. A `mount(host)` export in
|
|
80
92
|
`custom.js` can access the built-in page's session, draft and view controls.
|
|
81
93
|
|
|
94
|
+
Backend module ownership and the preserved session interfaces are documented in
|
|
95
|
+
the [runtime guide](../../docs/RUNTIME.md#workspace-session-api).
|
|
96
|
+
File/Git source resolution and view storage retain their existing public classes;
|
|
97
|
+
see [content views](../../docs/RUNTIME.md#workspace-content-views).
|
|
98
|
+
|
|
82
99
|
Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
|
|
83
100
|
in the view area, then choose features by name or ID in chat. The Agent adapts
|
|
84
101
|
their source into your frontend. To share a local change, ask it to publish a
|
|
@@ -133,6 +150,11 @@ token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
|
|
|
133
150
|
`/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
|
|
134
151
|
Other workspace features work without the SDK.
|
|
135
152
|
|
|
153
|
+
The adapter separates card rendering, refresh-token storage and SDK connections.
|
|
154
|
+
`PycodexCard` and `PycodexRuntimeLink` keep their existing imports and interfaces;
|
|
155
|
+
see [connection lifecycle](../../docs/RUNTIME.md#feishu-connection-lifecycle)
|
|
156
|
+
and [event presentation](../../docs/RUNTIME.md#observation).
|
|
157
|
+
|
|
136
158
|
## Development
|
|
137
159
|
|
|
138
160
|
From the repository root:
|
|
@@ -144,6 +166,9 @@ uv run --dev pytest packages/pycodex-ws/tests
|
|
|
144
166
|
uv build packages/pycodex-ws
|
|
145
167
|
```
|
|
146
168
|
|
|
147
|
-
Python integrations
|
|
148
|
-
|
|
169
|
+
Python integrations construct a runtime through `AgentsManager.create_root_runtime()`
|
|
170
|
+
and call `pycodex_ws.register_feishu_commands(runtime)` to include `/link` and
|
|
171
|
+
`/unlink`. The context indicator shows tokens against the auto-compaction limit;
|
|
172
|
+
its expandable Agent tree shows each task's status and latest assistant text.
|
|
173
|
+
See [runtime contracts](../../docs/RUNTIME.md) and
|
|
149
174
|
[test documentation](../../tests/TESTS.md).
|
|
@@ -15,15 +15,15 @@ from starlette.requests import HTTPConnection
|
|
|
15
15
|
from starlette.responses import HTMLResponse, JSONResponse
|
|
16
16
|
from starlette.routing import Mount, Route, WebSocketRoute
|
|
17
17
|
from starlette.websockets import WebSocketDisconnect, WebSocketState
|
|
18
|
-
from pycodex.portable.
|
|
18
|
+
from pycodex.portable.protocol import base58_encode
|
|
19
19
|
from pycodex.utils.session_persist import resolve_codex_home
|
|
20
|
-
from
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
from pycodex_ws.server_support import (
|
|
21
|
+
auth_cookie_matches,
|
|
22
|
+
install_auth,
|
|
23
|
+
json_object_body,
|
|
24
24
|
parse_listen,
|
|
25
25
|
)
|
|
26
|
-
from
|
|
26
|
+
from pycodex_ws.workspace_config import normalize_workspace_id
|
|
27
27
|
|
|
28
28
|
from .browser import BrowserAccess, require_login
|
|
29
29
|
from .protocol import (
|
|
@@ -52,7 +52,7 @@ def load_registration_key(path):
|
|
|
52
52
|
except FileExistsError:
|
|
53
53
|
value = path.read_bytes().strip()
|
|
54
54
|
else:
|
|
55
|
-
value =
|
|
55
|
+
value = base58_encode(os.urandom(16)).encode("ascii")
|
|
56
56
|
with os.fdopen(descriptor, "wb") as handle:
|
|
57
57
|
handle.write(value + b"\n")
|
|
58
58
|
if not re.fullmatch(b"[1-9A-HJ-NP-Za-km-z]{1,22}", value):
|
|
@@ -162,7 +162,7 @@ class Router:
|
|
|
162
162
|
self.api_url = normalize_api_url(api_url) if api_url is not None else None
|
|
163
163
|
self.api_port = api_port
|
|
164
164
|
self.registration_key = (
|
|
165
|
-
|
|
165
|
+
base58_encode(os.urandom(16))
|
|
166
166
|
if registration_key is None
|
|
167
167
|
else registration_key
|
|
168
168
|
)
|
|
@@ -208,11 +208,11 @@ class Router:
|
|
|
208
208
|
self.aliases = aliases
|
|
209
209
|
|
|
210
210
|
async def reorder_devices(self, request):
|
|
211
|
-
if not self.browser_token or not
|
|
211
|
+
if not self.browser_token or not auth_cookie_matches(
|
|
212
212
|
self.browser_token, request.cookies.get("pycodex_router_auth")
|
|
213
213
|
):
|
|
214
214
|
raise HTTPException(401, "Authentication required.")
|
|
215
|
-
payload = await
|
|
215
|
+
payload = await json_object_body(request, max_bytes=64 * 1024)
|
|
216
216
|
order = payload.get("order")
|
|
217
217
|
if (
|
|
218
218
|
not isinstance(order, list)
|
|
@@ -235,14 +235,14 @@ class Router:
|
|
|
235
235
|
return JSONResponse({"order": order})
|
|
236
236
|
|
|
237
237
|
async def rename_device(self, request):
|
|
238
|
-
if not self.browser_token or not
|
|
238
|
+
if not self.browser_token or not auth_cookie_matches(
|
|
239
239
|
self.browser_token, request.cookies.get("pycodex_router_auth")
|
|
240
240
|
):
|
|
241
241
|
raise HTTPException(401, "Authentication required.")
|
|
242
242
|
node_id = request.path_params["node_id"]
|
|
243
243
|
if node_id not in self.nodes:
|
|
244
244
|
raise HTTPException(404, "Device is not connected.")
|
|
245
|
-
payload = await
|
|
245
|
+
payload = await json_object_body(request)
|
|
246
246
|
try:
|
|
247
247
|
alias = normalize_alias(payload.get("alias"))
|
|
248
248
|
except ValueError as exc:
|
|
@@ -407,7 +407,7 @@ class Router:
|
|
|
407
407
|
agent_access = trusted_agent_request(scope)
|
|
408
408
|
if blocked or not (
|
|
409
409
|
agent_access
|
|
410
|
-
or
|
|
410
|
+
or auth_cookie_matches(
|
|
411
411
|
self.browser_token, connection.cookies.get("pycodex_router_auth")
|
|
412
412
|
)
|
|
413
413
|
):
|
|
@@ -535,7 +535,7 @@ def create_app(
|
|
|
535
535
|
)
|
|
536
536
|
|
|
537
537
|
async def registration_key_info(request):
|
|
538
|
-
if not router.browser_token or not
|
|
538
|
+
if not router.browser_token or not auth_cookie_matches(
|
|
539
539
|
router.browser_token, request.cookies.get("pycodex_router_auth")
|
|
540
540
|
):
|
|
541
541
|
return JSONResponse({"error": "authentication required"}, status_code=401)
|
|
@@ -553,7 +553,7 @@ def create_app(
|
|
|
553
553
|
Route("/api/nodes/{node_id}/alias", router.rename_device, methods=["POST"]),
|
|
554
554
|
]
|
|
555
555
|
)
|
|
556
|
-
router.browser_token =
|
|
556
|
+
router.browser_token = install_auth(
|
|
557
557
|
web,
|
|
558
558
|
password,
|
|
559
559
|
cookie_name="pycodex_router_auth",
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from importlib import import_module
|
|
4
4
|
|
|
5
|
-
from pycodex.bootstrap import build_runtime as build_core_runtime
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
runtime = build_core_runtime(agent)
|
|
6
|
+
def register_feishu_commands(runtime):
|
|
7
|
+
"""Attach Feishu commands and connection cleanup to an existing runtime."""
|
|
10
8
|
link = None
|
|
11
9
|
|
|
12
10
|
async def unlink(argument):
|
|
@@ -52,4 +50,3 @@ def build_runtime(agent):
|
|
|
52
50
|
runtime.register_command("link", connect)
|
|
53
51
|
runtime.register_command("unlink", unlink)
|
|
54
52
|
runtime.add_close_handler(close_link)
|
|
55
|
-
return runtime
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Shared Feishu endpoints and refresh-token storage."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
|
8
|
+
FEISHU_DOMAIN = "https://open.feishu.cn"
|
|
9
|
+
FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def refresh_token_path() -> Path:
|
|
13
|
+
return Path(FEISHU_REFRESH_TOKEN_FILE).expanduser()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_refresh_token() -> "typing.Union[str, None]":
|
|
17
|
+
path = refresh_token_path()
|
|
18
|
+
if not path.exists():
|
|
19
|
+
return None
|
|
20
|
+
value = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
21
|
+
return value or None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write_refresh_token(value: str) -> None:
|
|
25
|
+
path = refresh_token_path()
|
|
26
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
28
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
29
|
+
path.chmod(0o600)
|
|
30
|
+
stream.write("{0}\n".format(value))
|
|
@@ -2,7 +2,6 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
4
|
import typing
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
|
|
7
6
|
import requests
|
|
8
7
|
|
|
@@ -22,12 +21,21 @@ from pycodex.events import (
|
|
|
22
21
|
)
|
|
23
22
|
from pycodex.utils.event_helpers import completed_history
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
from .feishu_auth import (
|
|
25
|
+
FEISHU_API_BASE,
|
|
26
|
+
FEISHU_DOMAIN,
|
|
27
|
+
FEISHU_REFRESH_TOKEN_FILE,
|
|
28
|
+
read_refresh_token as _read_refresh_token,
|
|
29
|
+
refresh_token_path as _refresh_token_path,
|
|
30
|
+
write_refresh_token as _write_refresh_token,
|
|
31
|
+
)
|
|
32
|
+
from .feishu_render import (
|
|
33
|
+
CARD_OUTPUT_LIMIT,
|
|
34
|
+
CARD_OUTPUT_MODE_CODE,
|
|
35
|
+
CARD_OUTPUT_MODE_MARKDOWN,
|
|
36
|
+
render_card,
|
|
37
|
+
)
|
|
38
|
+
|
|
31
39
|
CARD_CONTENT_ERROR_MARKER = "Failed to create card content"
|
|
32
40
|
|
|
33
41
|
|
|
@@ -240,97 +248,7 @@ class PycodexCard:
|
|
|
240
248
|
def render(
|
|
241
249
|
self, output_mode: str = CARD_OUTPUT_MODE_MARKDOWN
|
|
242
250
|
) -> "typing.Dict[str, object]":
|
|
243
|
-
|
|
244
|
-
input_disabled = not self.accepts_input or self.detached or self.display.closed
|
|
245
|
-
output = _truncate(self.output_text, CARD_OUTPUT_LIMIT) or "Ready."
|
|
246
|
-
working_output = _truncate(self.working_output, CARD_OUTPUT_LIMIT)
|
|
247
|
-
color = _status_color("Detached" if self.detached else status or "Idle")
|
|
248
|
-
body_elements = [
|
|
249
|
-
_output_box(
|
|
250
|
-
"answer_box",
|
|
251
|
-
"answer_md",
|
|
252
|
-
_render_output_content(output, output_mode),
|
|
253
|
-
"grey-50",
|
|
254
|
-
),
|
|
255
|
-
]
|
|
256
|
-
if working_output:
|
|
257
|
-
body_elements.append(
|
|
258
|
-
_output_box(
|
|
259
|
-
"working_output_box",
|
|
260
|
-
"working_output_md",
|
|
261
|
-
_render_output_content(working_output, output_mode),
|
|
262
|
-
"green-50",
|
|
263
|
-
)
|
|
264
|
-
)
|
|
265
|
-
if self.activity_text:
|
|
266
|
-
body_elements.append(
|
|
267
|
-
{
|
|
268
|
-
"tag": "markdown",
|
|
269
|
-
"element_id": "activity_md",
|
|
270
|
-
"content": _render_output_content(
|
|
271
|
-
_truncate(self.activity_text, CARD_OUTPUT_LIMIT), output_mode
|
|
272
|
-
),
|
|
273
|
-
}
|
|
274
|
-
)
|
|
275
|
-
if self.user_prompt:
|
|
276
|
-
body_elements.insert(
|
|
277
|
-
0,
|
|
278
|
-
{
|
|
279
|
-
"tag": "markdown",
|
|
280
|
-
"element_id": "user_prompt_md",
|
|
281
|
-
"content": _render_output_content(
|
|
282
|
-
"user> " + _truncate(self.user_prompt, CARD_OUTPUT_LIMIT),
|
|
283
|
-
output_mode,
|
|
284
|
-
),
|
|
285
|
-
},
|
|
286
|
-
)
|
|
287
|
-
history = self.earlier_turns()
|
|
288
|
-
if history:
|
|
289
|
-
index = (
|
|
290
|
-
len(history) - 1 if self.history_index is None else self.history_index
|
|
291
|
-
)
|
|
292
|
-
body_elements.insert(
|
|
293
|
-
0,
|
|
294
|
-
_history_panel(
|
|
295
|
-
history,
|
|
296
|
-
index,
|
|
297
|
-
self.history_index is not None,
|
|
298
|
-
output_mode,
|
|
299
|
-
not self.detached,
|
|
300
|
-
),
|
|
301
|
-
)
|
|
302
|
-
card = {
|
|
303
|
-
"schema": "2.0",
|
|
304
|
-
"config": {"update_multi": True},
|
|
305
|
-
"header": {
|
|
306
|
-
"title": {
|
|
307
|
-
"tag": "plain_text",
|
|
308
|
-
"content": self.display.title or "pycodex",
|
|
309
|
-
},
|
|
310
|
-
"template": color,
|
|
311
|
-
},
|
|
312
|
-
"body": {
|
|
313
|
-
"elements": body_elements,
|
|
314
|
-
},
|
|
315
|
-
}
|
|
316
|
-
if not self.detached:
|
|
317
|
-
card["body"]["elements"].append(
|
|
318
|
-
{
|
|
319
|
-
"tag": "input",
|
|
320
|
-
"element_id": "prompt_input",
|
|
321
|
-
"name": "prompt",
|
|
322
|
-
"input_type": "text",
|
|
323
|
-
"width": "fill",
|
|
324
|
-
"disabled": input_disabled,
|
|
325
|
-
"placeholder": {
|
|
326
|
-
"tag": "plain_text",
|
|
327
|
-
"content": self.prompt_text + (status or self.model_name),
|
|
328
|
-
},
|
|
329
|
-
"behaviors": [{"type": "callback", "value": {"action": "send"}}],
|
|
330
|
-
"value": {"action": "send"},
|
|
331
|
-
},
|
|
332
|
-
)
|
|
333
|
-
return card
|
|
251
|
+
return render_card(self, output_mode)
|
|
334
252
|
|
|
335
253
|
def earlier_turns(self):
|
|
336
254
|
if self._active_turn_id is not None:
|
|
@@ -389,9 +307,7 @@ class PycodexCard:
|
|
|
389
307
|
aliases = _user_id_aliases()
|
|
390
308
|
if normalized in aliases:
|
|
391
309
|
return aliases[normalized]
|
|
392
|
-
if normalized.startswith("ou_"):
|
|
393
|
-
return normalized
|
|
394
|
-
if normalized.startswith("oc_"):
|
|
310
|
+
if normalized.startswith(("ou_", "oc_")):
|
|
395
311
|
return normalized
|
|
396
312
|
if normalized.isnumeric():
|
|
397
313
|
return self._lookup_user({"mobiles": [normalized]})
|
|
@@ -477,7 +393,7 @@ class PycodexCard:
|
|
|
477
393
|
use_user_token=use_user_token,
|
|
478
394
|
)
|
|
479
395
|
except Exception as exc:
|
|
480
|
-
if not
|
|
396
|
+
if CARD_CONTENT_ERROR_MARKER not in str(exc):
|
|
481
397
|
raise
|
|
482
398
|
return self._request(
|
|
483
399
|
method,
|
|
@@ -616,126 +532,6 @@ def _display_user_name(user: "typing.Any") -> "typing.Union[str, None]":
|
|
|
616
532
|
return None
|
|
617
533
|
|
|
618
534
|
|
|
619
|
-
def _status_color(status: str) -> str:
|
|
620
|
-
normalized = status.lower()
|
|
621
|
-
if normalized in {"detached", "closed"}:
|
|
622
|
-
return "grey"
|
|
623
|
-
if normalized in {"idle", "idle: sleeping"}:
|
|
624
|
-
return "green"
|
|
625
|
-
return "blue"
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
def _truncate(text: str, limit: int) -> str:
|
|
629
|
-
value = str(text or "")
|
|
630
|
-
if len(value) <= limit:
|
|
631
|
-
return value
|
|
632
|
-
return value[: max(0, limit - 32)] + "\n...[truncated]"
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
def _escape_code_block(text: str) -> str:
|
|
636
|
-
return str(text or "").replace("```", "'''")
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def _render_output_content(output: str, output_mode: str) -> str:
|
|
640
|
-
if output_mode == CARD_OUTPUT_MODE_CODE:
|
|
641
|
-
return "```text\n{0}\n```".format(_escape_code_block(output))
|
|
642
|
-
return output
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
def _history_panel(history, index, expanded, output_mode, interactive):
|
|
646
|
-
prompt, response = history[index]
|
|
647
|
-
content = _truncate(
|
|
648
|
-
"user> {0}\n\nassistant> {1}".format(prompt, response), CARD_OUTPUT_LIMIT
|
|
649
|
-
)
|
|
650
|
-
columns = []
|
|
651
|
-
for label, target, disabled in (
|
|
652
|
-
("Previous", index - 1, index == 0),
|
|
653
|
-
("Next", index + 1, index == len(history) - 1),
|
|
654
|
-
):
|
|
655
|
-
columns.append(
|
|
656
|
-
{
|
|
657
|
-
"tag": "column",
|
|
658
|
-
"width": "auto",
|
|
659
|
-
"elements": [
|
|
660
|
-
{
|
|
661
|
-
"tag": "button",
|
|
662
|
-
"text": {"tag": "plain_text", "content": label},
|
|
663
|
-
"type": "default",
|
|
664
|
-
"disabled": disabled or not interactive,
|
|
665
|
-
"behaviors": [
|
|
666
|
-
{
|
|
667
|
-
"type": "callback",
|
|
668
|
-
"value": {
|
|
669
|
-
"action": "history",
|
|
670
|
-
"history_index": target,
|
|
671
|
-
},
|
|
672
|
-
}
|
|
673
|
-
],
|
|
674
|
-
}
|
|
675
|
-
],
|
|
676
|
-
}
|
|
677
|
-
)
|
|
678
|
-
return {
|
|
679
|
-
"tag": "collapsible_panel",
|
|
680
|
-
"element_id": "history_panel",
|
|
681
|
-
"expanded": expanded,
|
|
682
|
-
"header": {
|
|
683
|
-
"icon": {
|
|
684
|
-
"tag": "standard_icon",
|
|
685
|
-
"token": "down-small-ccm_outlined",
|
|
686
|
-
"size": "16px 16px",
|
|
687
|
-
},
|
|
688
|
-
"title": {
|
|
689
|
-
"tag": "plain_text",
|
|
690
|
-
"content": "History · {0} / {1}".format(index + 1, len(history)),
|
|
691
|
-
},
|
|
692
|
-
},
|
|
693
|
-
"elements": [
|
|
694
|
-
{
|
|
695
|
-
"tag": "markdown",
|
|
696
|
-
"element_id": "history_md",
|
|
697
|
-
"content": _render_output_content(content, output_mode),
|
|
698
|
-
},
|
|
699
|
-
{"tag": "column_set", "columns": columns},
|
|
700
|
-
],
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
def _output_box(
|
|
705
|
-
box_id: str,
|
|
706
|
-
markdown_id: str,
|
|
707
|
-
content: str,
|
|
708
|
-
background_style: str,
|
|
709
|
-
) -> "typing.Dict[str, object]":
|
|
710
|
-
return {
|
|
711
|
-
"tag": "column_set",
|
|
712
|
-
"element_id": box_id,
|
|
713
|
-
"background_style": background_style,
|
|
714
|
-
"horizontal_spacing": "8px",
|
|
715
|
-
"horizontal_align": "left",
|
|
716
|
-
"columns": [
|
|
717
|
-
{
|
|
718
|
-
"tag": "column",
|
|
719
|
-
"width": "auto",
|
|
720
|
-
"elements": [
|
|
721
|
-
{
|
|
722
|
-
"tag": "markdown",
|
|
723
|
-
"element_id": markdown_id,
|
|
724
|
-
"content": content,
|
|
725
|
-
},
|
|
726
|
-
],
|
|
727
|
-
"vertical_spacing": "8px",
|
|
728
|
-
"horizontal_align": "left",
|
|
729
|
-
"vertical_align": "top",
|
|
730
|
-
}
|
|
731
|
-
],
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
def _is_card_content_error(exc: "BaseException") -> bool:
|
|
736
|
-
return CARD_CONTENT_ERROR_MARKER in str(exc)
|
|
737
|
-
|
|
738
|
-
|
|
739
535
|
def _dig(value: "typing.Any", *path: str) -> "typing.Any":
|
|
740
536
|
current = value
|
|
741
537
|
for key in path:
|
|
@@ -753,27 +549,6 @@ def _env(*names: str) -> "typing.Union[str, None]":
|
|
|
753
549
|
return None
|
|
754
550
|
|
|
755
551
|
|
|
756
|
-
def _refresh_token_path() -> Path:
|
|
757
|
-
return Path(FEISHU_REFRESH_TOKEN_FILE).expanduser()
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
def _read_refresh_token() -> "typing.Union[str, None]":
|
|
761
|
-
path = _refresh_token_path()
|
|
762
|
-
if not path.exists():
|
|
763
|
-
return None
|
|
764
|
-
value = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
765
|
-
return value or None
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
def _write_refresh_token(value: str) -> None:
|
|
769
|
-
path = _refresh_token_path()
|
|
770
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
771
|
-
descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
772
|
-
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
773
|
-
path.chmod(0o600)
|
|
774
|
-
stream.write("{0}\n".format(value))
|
|
775
|
-
|
|
776
|
-
|
|
777
552
|
def _api_base_to_domain(api_base: str) -> str:
|
|
778
553
|
base = str(api_base or FEISHU_API_BASE).rstrip("/")
|
|
779
554
|
marker = "/open-apis"
|