@turmind/halo 1.5.7 → 1.5.9
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/220.7566bc89f3d98993.js +1 -0
- package/admin-out/_next/static/chunks/597-399e2e1de26aa747.js +1 -0
- package/admin-out/_next/static/chunks/989-c404f6f43c400911.js +1 -0
- package/admin-out/_next/static/chunks/app/page-59bfce35bbb30b05.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-edf248e07333566d.js → webpack-efd27999305b080a.js} +1 -1
- package/admin-out/_next/static/css/27b44c85485eabb9.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/architecture.md +3 -2
- package/bundled-docs/design/canvas-extensions.md +51 -11
- package/bundled-docs/design/cron.md +6 -6
- package/bundled-docs/design/evolution.md +47 -15
- package/bundled-docs/design/express-self.md +96 -20
- package/bundled-docs/design/storage.md +10 -2
- package/bundled-docs/design/ws.md +1 -0
- package/bundled-docs/dev/add-model-provider.md +16 -7
- package/bundled-docs/dev/api.md +7 -6
- package/bundled-docs/dev/desktop-packaging.md +26 -0
- package/bundled-docs/guide/agents.md +3 -3
- package/bundled-docs/guide/chat.md +3 -0
- package/bundled-docs/guide/cli.md +10 -1
- package/bundled-docs/guide/workspace.md +2 -2
- package/bundled-docs/requirements/agent.md +1 -1
- package/bundled-docs/requirements/chat.md +17 -9
- package/bundled-docs/requirements/editor.md +3 -1
- package/bundled-docs/requirements/explorer.md +4 -2
- package/bundled-docs/requirements/settings.md +11 -3
- package/bundled-docs/requirements/workspace.md +2 -2
- package/dist/index.js +2514 -1669
- package/package.json +2 -1
- package/templates/agents/__evo_agent__/AGENT.md +7 -0
- package/templates/agents/__score__/AGENT.md +183 -71
- package/templates/canvas/self.html +617 -71
- package/templates/models/anthropic.yaml +1 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +1 -0
- package/templates/models/aws-bedrock-mantle.yaml +1 -0
- package/templates/models/aws-bedrock-openai.yaml +1 -0
- package/templates/models/deepseek.yaml +1 -0
- package/templates/models/doubao.yaml +1 -0
- package/templates/models/hunyuan.yaml +1 -0
- package/templates/models/kimi.yaml +1 -0
- package/templates/models/mimo-token-plan-china.yaml +1 -0
- package/templates/models/minimax.yaml +1 -0
- package/templates/models/openai.yaml +1 -0
- package/templates/models/qwen.yaml +1 -0
- package/templates/models/zhipu.yaml +1 -0
- package/templates/skills/cron/SKILL.md +59 -4
- package/templates/skills/cron/manage-cron.py +158 -24
- package/templates/skills/extension/SKILL.md +30 -2
- package/templates/skills/extension/config.yaml +2 -2
- package/templates/skills/extension/templates/ext.sh +69 -16
- package/templates/skills/self/SKILL.md +79 -17
- package/admin-out/_next/static/chunks/220.e250bbbf65d24094.js +0 -1
- package/admin-out/_next/static/chunks/422-cc74b1fdd3af5415.js +0 -1
- package/admin-out/_next/static/chunks/989-e0faeff1c4a0b0b9.js +0 -1
- package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +0 -1
- package/admin-out/_next/static/css/b8c87b919758c52b.css +0 -3
- /package/admin-out/_next/static/{rTD3zkMvzwtqUwzhz6QJH → RD5w3FmDZ6HtyQGGaUb8t}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{rTD3zkMvzwtqUwzhz6QJH → RD5w3FmDZ6HtyQGGaUb8t}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
id: aws-bedrock-claude-invoke
|
|
2
2
|
runtime: bedrock-invoke
|
|
3
|
+
revision: 2026100501
|
|
3
4
|
displayName: AWS Bedrock Claude (Invoke API)
|
|
4
5
|
displayName_zh: AWS Bedrock Claude(Invoke API)
|
|
5
6
|
description: Invokes Bedrock via InvokeModel (non-streaming), sends raw Anthropic Messages API format
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
id: aws-bedrock-mantle
|
|
2
2
|
runtime: bedrock-mantle
|
|
3
|
+
revision: 2026100501
|
|
3
4
|
displayName: AWS Bedrock Mantle (OpenAI GPT-6.1 / 6 / 5.6)
|
|
4
5
|
displayName_zh: AWS Bedrock Mantle(OpenAI GPT-6.1 / 6 / 5.6)
|
|
5
6
|
description: OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna) on Amazon Bedrock via the bedrock-mantle endpoint (OpenAI Responses API only — not Converse/InvokeModel). Region-split — GPT-6.1 Sol and GPT-6 Sol / Luna are on us-east-1 only, Astra on us-west-2 only.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
id: aws-bedrock-openai
|
|
2
2
|
runtime: bedrock-mantle
|
|
3
|
+
revision: 2026100501
|
|
3
4
|
displayName: AWS Bedrock OpenAI-compatible (GPT-6.1 / 6 / 5.6 · Grok 4.6 · Kimi K3)
|
|
4
5
|
displayName_zh: AWS Bedrock OpenAI 兼容接口(GPT-6.1 / 6 / 5.6 · Grok 4.6 · Kimi K3)
|
|
5
6
|
description: Frontier models on Amazon Bedrock via the bedrock-runtime endpoint's OpenAI Responses API (/openai/v1) — OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna), xAI Grok 4.6, Moonshot Kimi K3. Cross-Region inference profiles (global.*) only — every region serves every model, no region split. No server-side web_search (that is Mantle-only).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
id: mimo-token-plan-china
|
|
2
2
|
runtime: anthropic-messages
|
|
3
|
+
revision: 2026100501
|
|
3
4
|
displayName: Xiaomi MiMo
|
|
4
5
|
displayName_zh: 小米 MiMo
|
|
5
6
|
description: Xiaomi MiMo via its Anthropic-compatible gateway — Token Plan (China / Singapore / Europe clusters) or pay-as-you-go. Supports 1M context window.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
id: openai
|
|
2
2
|
runtime: openai-chat
|
|
3
|
+
revision: 2026100501
|
|
3
4
|
displayName: OpenAI (Generic)
|
|
4
5
|
displayName_zh: OpenAI(通用)
|
|
5
6
|
description: Generic OpenAI-compatible chat completions. Use for native OpenAI or any third-party OpenAI-compatible gateway. Capabilities are exposed openly — the user is expected to know what their endpoint supports.
|
|
@@ -38,6 +38,12 @@ loop). No restart needed after edits.
|
|
|
38
38
|
the result back to the current chat with `{{channel.chat_id}}` — the user
|
|
39
39
|
almost always means "send it to me here"
|
|
40
40
|
- **edit / pause / resume** → `update`, `enable`, `disable`
|
|
41
|
+
- **skip / pause until a date / resume** ("no A-share report over the holiday",
|
|
42
|
+
"skip this week", "send until month end", "start again") → `update <id>
|
|
43
|
+
--active-from …` / `--active-until …` (see *Active window* in step 2).
|
|
44
|
+
A pause with a known end date goes through `--active-from`, **not**
|
|
45
|
+
`disable` — a disabled job only comes back if someone remembers to
|
|
46
|
+
re-enable it
|
|
41
47
|
- **delete / remove** → `delete`. From inside a channel the user won't
|
|
42
48
|
know the cron `id`; first run `list --chat-id {{channel.chat_id}}` to find
|
|
43
49
|
the matching job, confirm which one, then delete by id
|
|
@@ -54,7 +60,7 @@ Sensible defaults if user didn't say:
|
|
|
54
60
|
| targets | none (log only — the run shows in admin Cron tab) |
|
|
55
61
|
| label | summarize from prompt + schedule |
|
|
56
62
|
| timeout | leave unset (3600s) — see `--timeout-sec` below |
|
|
57
|
-
| session | leave unset (the job's own
|
|
63
|
+
| session | leave unset (the job's own session) — see `--session` below |
|
|
58
64
|
| prompt | pass the user's task as-is — the runner prepends an "unattended run, don't ask questions" notice to every fire, so don't add one yourself |
|
|
59
65
|
|
|
60
66
|
### 2. Translate the schedule
|
|
@@ -89,7 +95,9 @@ heavy analysis run that needs more than an hour (`--timeout-sec 7200`). On
|
|
|
89
95
|
`update`, pass `--timeout-sec ""` to clear back to the default.
|
|
90
96
|
|
|
91
97
|
**Run inside an existing session (`--session`, optional)** — by default every
|
|
92
|
-
job accumulates its own
|
|
98
|
+
job accumulates its own session (same id as the job; older jobs keep the
|
|
99
|
+
`cron-cron-…` session they already have, shown as their `session_id`).
|
|
100
|
+
Pass `--session <rootSessionId>`
|
|
93
101
|
only when the user wants the runs to continue a specific conversation (the
|
|
94
102
|
agent then sees that session's history). Rules the user should hear once:
|
|
95
103
|
- the session keeps **its own agent and access level** — `--agent` only
|
|
@@ -100,7 +108,31 @@ agent then sees that session's history). Rules the user should hear once:
|
|
|
100
108
|
|
|
101
109
|
Root session ids only (no `>` sub-session paths). You don't know session ids
|
|
102
110
|
yourself — ask the user (admin → Sessions) rather than guessing. On `update`,
|
|
103
|
-
pass `--session ""` to go back to the job's own session
|
|
111
|
+
pass `--session ""` to go back to the job's own session (for an older job
|
|
112
|
+
pinned to `cron-cron-…` that starts a fresh session — leave it alone unless
|
|
113
|
+
the user asks).
|
|
114
|
+
|
|
115
|
+
**Active window (`--active-from` / `--active-until`, optional, recurring
|
|
116
|
+
jobs only)** — the schedule fires only inside `[from, until)`; fires outside
|
|
117
|
+
it are skipped silently (no run recorded). "Run now" ignores the window.
|
|
118
|
+
- Values: `YYYY-MM-DD`, ISO-8601 (`2026-10-08T09:00`) or unix ms. A time
|
|
119
|
+
without an offset is read in the **job's timezone** (on `update`: the new
|
|
120
|
+
`--timezone` if you pass one, else the job's stored one; host time if
|
|
121
|
+
the job has none).
|
|
122
|
+
- A bare date means **that day 00:00** for `--active-from`, and **the next
|
|
123
|
+
day 00:00** for `--active-until` — the date itself is included
|
|
124
|
+
(`--active-until 2026-10-31` still fires on the 31st).
|
|
125
|
+
- `--active-from` may be in the past (= active now); `--active-until` must
|
|
126
|
+
be in the future and after `--active-from`. `""` clears either bound.
|
|
127
|
+
- Past `--active-until` the job stays enabled but stops firing; the admin
|
|
128
|
+
shows it as "ended". Extending `--active-until` brings it back.
|
|
129
|
+
- Rejected together with `--run-at` (one-shot jobs have no window).
|
|
130
|
+
- There is **no "skip a middle stretch, keep firing before it"**: one
|
|
131
|
+
window per job. If the user asks for that (e.g. "keep sending this week,
|
|
132
|
+
skip next week, then resume"), say so plainly and offer a workaround —
|
|
133
|
+
usually: leave it running now and push `--active-from` past the gap just
|
|
134
|
+
before the gap starts (a one-shot cron can remind you), or create a
|
|
135
|
+
second job for the earlier stretch with an `--active-until`.
|
|
104
136
|
|
|
105
137
|
### 3. Channels (only when delivery is wanted)
|
|
106
138
|
|
|
@@ -188,12 +220,27 @@ shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py update <id> \
|
|
|
188
220
|
--schedule "0 10 * * 1"
|
|
189
221
|
```
|
|
190
222
|
|
|
191
|
-
**Pause / unpause:**
|
|
223
|
+
**Pause / unpause (no end date in mind):**
|
|
192
224
|
```bash
|
|
193
225
|
shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py disable <id>
|
|
194
226
|
shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py enable <id>
|
|
195
227
|
```
|
|
196
228
|
|
|
229
|
+
**Skip / pause until a date / resume (active window):** find the job first
|
|
230
|
+
(`list`, or `list --chat-id {{channel.chat_id}}` inside a chat), then:
|
|
231
|
+
```bash
|
|
232
|
+
# "Don't send the English weekly this week" → active from next Monday
|
|
233
|
+
shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py update <id> --active-from 2026-10-12
|
|
234
|
+
# "No A-share report over the National Day holiday" → resume on Oct 8
|
|
235
|
+
shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py update <id> --active-from 2026-10-08
|
|
236
|
+
# "Send it until the end of the month" → Oct 31 still fires
|
|
237
|
+
shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py update <id> --active-until 2026-10-31
|
|
238
|
+
# "Start sending again" → clear the pause
|
|
239
|
+
shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py update <id> --active-from ""
|
|
240
|
+
```
|
|
241
|
+
Prefer this over `disable` whenever the pause has a known end — it resumes
|
|
242
|
+
on its own.
|
|
243
|
+
|
|
197
244
|
**Delete:**
|
|
198
245
|
```bash
|
|
199
246
|
shell_exec: python3 ~/.halo/global/skills/cron/manage-cron.py delete <id>
|
|
@@ -229,6 +276,14 @@ picks it up within ~10s. Example:
|
|
|
229
276
|
> Created cron job `cron-mphsy-abc123`: runs the `default` agent every day at 9am,
|
|
230
277
|
> output will be pushed to this chat. The runner picks it up within ~10s.
|
|
231
278
|
|
|
279
|
+
For an active-window change, say when it resumes / stops — read
|
|
280
|
+
`active_from_iso` / `active_until_iso` back with `get <id>` and name the
|
|
281
|
+
first fire on or after it from the schedule (`get` has no computed next-run
|
|
282
|
+
field; the admin Cron tab shows it):
|
|
283
|
+
|
|
284
|
+
> Paused `A-share daily brief` over the holiday: it resumes on Oct 8 —
|
|
285
|
+
> next run Oct 8 09:30 (Asia/Shanghai). Nothing is sent before that.
|
|
286
|
+
|
|
232
287
|
## Patterns that go sideways
|
|
233
288
|
|
|
234
289
|
- **telegram / slack / feishu / wecom target without a chatId** — every fire errors with
|
|
@@ -32,6 +32,12 @@ Subcommands:
|
|
|
32
32
|
|
|
33
33
|
`--targets` arg: comma-separated `channelType:accountId` pairs.
|
|
34
34
|
example: `--targets telegram:halo_agent_bot,wechat:alice`
|
|
35
|
+
|
|
36
|
+
`--active-from` / `--active-until` (recurring jobs only): the schedule fires
|
|
37
|
+
only inside [from, until); fires outside it are skipped silently. Accepts
|
|
38
|
+
YYYY-MM-DD, ISO-8601 or unix ms; a time without offset is read in the job's
|
|
39
|
+
timezone. A bare date means that day 00:00 for --active-from and the NEXT
|
|
40
|
+
day 00:00 for --active-until (so the date itself is included). `""` clears.
|
|
35
41
|
"""
|
|
36
42
|
import argparse
|
|
37
43
|
import json
|
|
@@ -126,8 +132,8 @@ SESSION_ID_RE = re.compile(r'^[A-Za-z0-9_:-]{1,200}$')
|
|
|
126
132
|
|
|
127
133
|
def parse_session(raw: str | None) -> str | None:
|
|
128
134
|
"""Validate --session: a root session id (same rule as the admin REST
|
|
129
|
-
routes — no `>` sub-session path). Empty = None = the job's own
|
|
130
|
-
|
|
135
|
+
routes — no `>` sub-session path). Empty = None = the job's own session
|
|
136
|
+
(same id as the job)."""
|
|
131
137
|
if raw is None or not raw.strip():
|
|
132
138
|
return None
|
|
133
139
|
v = raw.strip()
|
|
@@ -157,6 +163,90 @@ def parse_run_at(raw: str | None) -> int | None:
|
|
|
157
163
|
return int(dt.timestamp() * 1000)
|
|
158
164
|
|
|
159
165
|
|
|
166
|
+
DATE_RE = re.compile(r'^\d{4}-\d{2}-\d{2}$')
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _tzinfo(tz_name: str | None):
|
|
170
|
+
"""ZoneInfo for an IANA name; None for unset or unknown names."""
|
|
171
|
+
if not tz_name:
|
|
172
|
+
return None
|
|
173
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
174
|
+
try:
|
|
175
|
+
return ZoneInfo(tz_name)
|
|
176
|
+
except (ZoneInfoNotFoundError, ValueError):
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def job_tz(tz_name: str | None):
|
|
181
|
+
"""tzinfo for the job's timezone; None = host-local."""
|
|
182
|
+
tz = _tzinfo(tz_name)
|
|
183
|
+
if tz_name and tz is None:
|
|
184
|
+
die(f'unknown timezone "{tz_name}"; expected an IANA name like Asia/Shanghai')
|
|
185
|
+
return tz
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_active_bound(flag: str, raw: str, tz_name: str | None) -> int | None:
|
|
189
|
+
"""Parse --active-from / --active-until into epoch ms; "" = None (clear).
|
|
190
|
+
|
|
191
|
+
YYYY-MM-DD → that day 00:00 (from) or the NEXT day 00:00 (until, so the
|
|
192
|
+
date is included). ISO without offset / bare dates are read in the job's
|
|
193
|
+
timezone (host-local when the job has none)."""
|
|
194
|
+
raw = raw.strip()
|
|
195
|
+
if not raw:
|
|
196
|
+
return None
|
|
197
|
+
if raw.isdigit():
|
|
198
|
+
return int(raw)
|
|
199
|
+
from datetime import datetime, timedelta
|
|
200
|
+
tz = job_tz(tz_name)
|
|
201
|
+
if DATE_RE.match(raw):
|
|
202
|
+
try:
|
|
203
|
+
dt = datetime.strptime(raw, '%Y-%m-%d')
|
|
204
|
+
except ValueError:
|
|
205
|
+
die(f'invalid {flag} "{raw}": not a calendar date')
|
|
206
|
+
if flag == '--active-until':
|
|
207
|
+
dt += timedelta(days=1)
|
|
208
|
+
else:
|
|
209
|
+
try:
|
|
210
|
+
dt = datetime.fromisoformat(raw)
|
|
211
|
+
except ValueError:
|
|
212
|
+
die(f'invalid {flag} "{raw}": expected YYYY-MM-DD, ISO-8601 (e.g. 2026-10-08T09:00) or unix ms')
|
|
213
|
+
if dt.tzinfo is None:
|
|
214
|
+
dt = dt.replace(tzinfo=tz) if tz else dt.astimezone()
|
|
215
|
+
return int(dt.timestamp() * 1000)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def validate_active_window(run_at, active_from, active_until, new_until: bool):
|
|
219
|
+
"""Same rules as the admin REST routes, on the merged row: recurring jobs
|
|
220
|
+
only; a newly set --active-until must be in the future and after from."""
|
|
221
|
+
if active_from is None and active_until is None:
|
|
222
|
+
return
|
|
223
|
+
if run_at is not None:
|
|
224
|
+
die('--active-from/--active-until only apply to recurring jobs, not --run-at one-shots '
|
|
225
|
+
'(on update, clear them with --active-from "" --active-until "")')
|
|
226
|
+
if new_until and active_until is not None and active_until <= now_ms():
|
|
227
|
+
die('--active-until must be in the future')
|
|
228
|
+
if active_from is not None and active_until is not None and active_until <= active_from:
|
|
229
|
+
die('--active-until must be after --active-from')
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def require_active_columns(conn: sqlite3.Connection):
|
|
233
|
+
"""cron.db gains active_from/active_until via the server's boot migration;
|
|
234
|
+
a server that hasn't restarted onto this version yet won't have them."""
|
|
235
|
+
cols = {r[1] for r in conn.execute('PRAGMA table_info(cron_jobs)').fetchall()}
|
|
236
|
+
if 'active_from' not in cols or 'active_until' not in cols:
|
|
237
|
+
die('cron.db has no active_from/active_until columns yet — restart the halo server '
|
|
238
|
+
'once so it migrates cron.db, then retry')
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def ms_to_iso(ms, tz_name: str | None) -> str | None:
|
|
242
|
+
if ms is None:
|
|
243
|
+
return None
|
|
244
|
+
from datetime import datetime
|
|
245
|
+
tz = _tzinfo(tz_name) # display only — never die on a stored bad tz
|
|
246
|
+
dt = datetime.fromtimestamp(ms / 1000, tz=tz) if tz else datetime.fromtimestamp(ms / 1000).astimezone()
|
|
247
|
+
return dt.isoformat(timespec='minutes')
|
|
248
|
+
|
|
249
|
+
|
|
160
250
|
def row_to_job(row: sqlite3.Row) -> dict:
|
|
161
251
|
j = dict(row)
|
|
162
252
|
try:
|
|
@@ -164,6 +254,11 @@ def row_to_job(row: sqlite3.Row) -> dict:
|
|
|
164
254
|
except Exception:
|
|
165
255
|
j['targets'] = []
|
|
166
256
|
j['enabled'] = bool(j['enabled'])
|
|
257
|
+
# Active window: ms (as stored) + ISO in the job's timezone. Pre-migration
|
|
258
|
+
# dbs have no columns — report them as unset.
|
|
259
|
+
for k in ('active_from', 'active_until'):
|
|
260
|
+
j.setdefault(k, None)
|
|
261
|
+
j[f'{k}_iso'] = ms_to_iso(j[k], j.get('timezone'))
|
|
167
262
|
return j
|
|
168
263
|
|
|
169
264
|
|
|
@@ -222,6 +317,9 @@ def cmd_create(args):
|
|
|
222
317
|
die('--run-at must be in the future')
|
|
223
318
|
timeout_sec = parse_timeout_sec(args.timeout_sec)
|
|
224
319
|
session_id = parse_session(args.session)
|
|
320
|
+
active_from = parse_active_bound('--active-from', args.active_from or '', args.timezone)
|
|
321
|
+
active_until = parse_active_bound('--active-until', args.active_until or '', args.timezone)
|
|
322
|
+
validate_active_window(run_at_ms, active_from, active_until, new_until=True)
|
|
225
323
|
|
|
226
324
|
job_id = args.id or gen_job_id()
|
|
227
325
|
now = now_ms()
|
|
@@ -232,25 +330,31 @@ def cmd_create(args):
|
|
|
232
330
|
if existing:
|
|
233
331
|
die(f'job id {job_id} already exists; pick another with --id', 2)
|
|
234
332
|
|
|
333
|
+
values: dict[str, object] = {
|
|
334
|
+
'id': job_id,
|
|
335
|
+
'label': args.label,
|
|
336
|
+
'workspace_path': args.workspace,
|
|
337
|
+
'agent_id': args.agent or 'default',
|
|
338
|
+
'user_prompt': args.prompt,
|
|
339
|
+
'schedule': args.schedule or '',
|
|
340
|
+
'run_at': run_at_ms,
|
|
341
|
+
'timezone': args.timezone,
|
|
342
|
+
'timeout_sec': timeout_sec,
|
|
343
|
+
'session_id': session_id,
|
|
344
|
+
'targets': json.dumps(targets, ensure_ascii=False),
|
|
345
|
+
'enabled': 0 if args.disabled else 1,
|
|
346
|
+
'created_at': now,
|
|
347
|
+
'updated_at': now,
|
|
348
|
+
}
|
|
349
|
+
# Only name the window columns when used, so jobs without a window can
|
|
350
|
+
# still be created on a cron.db the server hasn't migrated yet.
|
|
351
|
+
if active_from is not None or active_until is not None:
|
|
352
|
+
require_active_columns(conn)
|
|
353
|
+
values['active_from'] = active_from
|
|
354
|
+
values['active_until'] = active_until
|
|
235
355
|
conn.execute(
|
|
236
|
-
'INSERT INTO cron_jobs(
|
|
237
|
-
|
|
238
|
-
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
239
|
-
(
|
|
240
|
-
job_id,
|
|
241
|
-
args.label,
|
|
242
|
-
args.workspace,
|
|
243
|
-
args.agent or 'default',
|
|
244
|
-
args.prompt,
|
|
245
|
-
args.schedule or '',
|
|
246
|
-
run_at_ms,
|
|
247
|
-
args.timezone,
|
|
248
|
-
timeout_sec,
|
|
249
|
-
session_id,
|
|
250
|
-
json.dumps(targets, ensure_ascii=False),
|
|
251
|
-
0 if args.disabled else 1,
|
|
252
|
-
now, now,
|
|
253
|
-
),
|
|
356
|
+
f'INSERT INTO cron_jobs({", ".join(values)}) VALUES ({", ".join("?" for _ in values)})',
|
|
357
|
+
list(values.values()),
|
|
254
358
|
)
|
|
255
359
|
conn.commit()
|
|
256
360
|
print(json.dumps({'id': job_id, 'created': True}, ensure_ascii=False))
|
|
@@ -279,12 +383,29 @@ def cmd_update(args):
|
|
|
279
383
|
# same convention as `--run-at ""`.
|
|
280
384
|
sets.append(('timeout_sec', parse_timeout_sec(args.timeout_sec)))
|
|
281
385
|
if args.session is not None:
|
|
282
|
-
# `--session ""` clears back to the job's own
|
|
386
|
+
# `--session ""` clears back to the job's own session (id = job id).
|
|
283
387
|
sets.append(('session_id', parse_session(args.session)))
|
|
284
388
|
if args.targets is not None:
|
|
285
389
|
sets.append(('targets', json.dumps(parse_targets(args.targets), ensure_ascii=False)))
|
|
390
|
+
cur = dict(row)
|
|
391
|
+
if args.active_from is not None or args.active_until is not None:
|
|
392
|
+
require_active_columns(conn)
|
|
393
|
+
# Bare dates / offset-less times read in the job's timezone after
|
|
394
|
+
# this update: the new --timezone if given, else the stored one.
|
|
395
|
+
tz = args.timezone if args.timezone is not None else cur.get('timezone')
|
|
396
|
+
if args.active_from is not None:
|
|
397
|
+
sets.append(('active_from', parse_active_bound('--active-from', args.active_from, tz)))
|
|
398
|
+
if args.active_until is not None:
|
|
399
|
+
sets.append(('active_until', parse_active_bound('--active-until', args.active_until, tz)))
|
|
400
|
+
# Window rules on the merged row (also catches --run-at on a job that
|
|
401
|
+
# still has a window).
|
|
402
|
+
merged = {**cur, **dict(sets)}
|
|
403
|
+
validate_active_window(
|
|
404
|
+
merged.get('run_at'), merged.get('active_from'), merged.get('active_until'),
|
|
405
|
+
new_until=bool(args.active_until and args.active_until.strip()),
|
|
406
|
+
)
|
|
286
407
|
if not sets:
|
|
287
|
-
die('nothing to update — pass at least one --label/--workspace/--agent/--prompt/--schedule/--run-at/--timezone/--timeout-sec/--session/--targets')
|
|
408
|
+
die('nothing to update — pass at least one --label/--workspace/--agent/--prompt/--schedule/--run-at/--timezone/--timeout-sec/--session/--targets/--active-from/--active-until')
|
|
288
409
|
sets.append(('updated_at', now_ms()))
|
|
289
410
|
|
|
290
411
|
placeholders = ', '.join(f'{k} = ?' for k, _ in sets)
|
|
@@ -432,10 +553,16 @@ def main():
|
|
|
432
553
|
c.add_argument('--timeout-sec', dest='timeout_sec',
|
|
433
554
|
help='max run time in seconds, 60–21600 (default: unset = 3600)')
|
|
434
555
|
c.add_argument('--session',
|
|
435
|
-
help="run in this existing root session id (default: the job's own
|
|
556
|
+
help="run in this existing root session id (default: the job's own session, same id as the job)")
|
|
436
557
|
c.add_argument('--targets',
|
|
437
558
|
help='comma-separated channelType:accountId[:chatId] list, or JSON array. '
|
|
438
559
|
'chatId pins delivery to a specific chat (e.g. when scheduling from inside a chat).')
|
|
560
|
+
c.add_argument('--active-from', dest='active_from',
|
|
561
|
+
help='recurring only: start firing from this time — YYYY-MM-DD (= that day 00:00), '
|
|
562
|
+
'ISO-8601 or unix ms; no offset = the job timezone. Use it to "pause until" a date.')
|
|
563
|
+
c.add_argument('--active-until', dest='active_until',
|
|
564
|
+
help='recurring only: stop firing at this time (exclusive) — YYYY-MM-DD (= the NEXT day 00:00, '
|
|
565
|
+
'so the date itself still fires), ISO-8601 or unix ms; must be in the future')
|
|
439
566
|
c.add_argument('--disabled', action='store_true', help='create paused (default: enabled)')
|
|
440
567
|
c.set_defaults(func=cmd_create)
|
|
441
568
|
|
|
@@ -451,8 +578,15 @@ def main():
|
|
|
451
578
|
u.add_argument('--timeout-sec', dest='timeout_sec',
|
|
452
579
|
help='max run time in seconds, 60–21600; pass "" to clear back to default 3600')
|
|
453
580
|
u.add_argument('--session',
|
|
454
|
-
help='root session id to run in; pass "" to clear back to
|
|
581
|
+
help='root session id to run in; pass "" to clear back to the job\'s own session '
|
|
582
|
+
'(same id as the job — an older job pinned to cron-cron-… then starts a fresh one)')
|
|
455
583
|
u.add_argument('--targets')
|
|
584
|
+
u.add_argument('--active-from', dest='active_from',
|
|
585
|
+
help='recurring only: YYYY-MM-DD (= that day 00:00), ISO-8601 or unix ms, in the job timezone; '
|
|
586
|
+
'pass "" to clear (= active now)')
|
|
587
|
+
u.add_argument('--active-until', dest='active_until',
|
|
588
|
+
help='recurring only: YYYY-MM-DD (= the NEXT day 00:00, date included), ISO-8601 or unix ms, '
|
|
589
|
+
'in the job timezone; pass "" to clear (= no end)')
|
|
456
590
|
u.set_defaults(func=cmd_update)
|
|
457
591
|
|
|
458
592
|
e = sub.add_parser('enable')
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: extension
|
|
3
|
-
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`). `/extension install <id|zip|url>` pulls the latest `<id>-v*` release from the configured hub (default turmind/halo-hub). Activate when the user wants to preview a file type the editor can't open,
|
|
3
|
+
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`) — and update the model provider list (new models, changed provider endpoints) from the hub. `/extension install <id|zip|url>` pulls the latest `<id>-v*` release from the configured hub (default turmind/halo-hub); `/extension models` pulls the latest `models-v*` provider configs. Activate when the user wants to preview a file type the editor can't open, asks to install / update / remove an extension, or asks to update the model list / get a newly released model ("更新模型列表").
|
|
4
4
|
command: /extension
|
|
5
5
|
requiresAccess: full
|
|
6
6
|
verbs:
|
|
7
7
|
- { name: install, desc: "Install or upgrade an extension from the hub (by id), a local zip path, or a zip URL" }
|
|
8
8
|
- { name: list, desc: List installed extensions with version and status }
|
|
9
9
|
- { name: remove, desc: Remove an installed extension by id }
|
|
10
|
+
- { name: models, desc: "Update the model provider list from the hub (newest models-v* release)" }
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# extension
|
|
13
14
|
|
|
14
15
|
The requested action is **`$1`**; everything after it is the payload
|
|
15
16
|
(`$ARGUMENTS` minus the first token). With natural language ("install the glb
|
|
16
|
-
viewer"), infer both.
|
|
17
|
+
viewer", "更新模型列表"), infer both.
|
|
17
18
|
|
|
18
19
|
Preview extensions are static bundles the admin editor loads in a sandboxed
|
|
19
20
|
iframe to preview file types it has no built-in viewer for (e.g. `.glb`).
|
|
@@ -70,6 +71,33 @@ short `-` list; `(none)` when empty.
|
|
|
70
71
|
`bash EXT remove <id>` → deletes `~/.halo/global/extensions/<id>/`. Confirm with
|
|
71
72
|
the user first if they didn't name the id explicitly.
|
|
72
73
|
|
|
74
|
+
## models
|
|
75
|
+
|
|
76
|
+
Updates the model provider list (the providers / models / endpoints the agent
|
|
77
|
+
editor offers) from the hub's newest `models-v*` release. Also run it for
|
|
78
|
+
natural language like "更新模型列表" / "update the model list" / "is there a
|
|
79
|
+
newer model". Run it right away:
|
|
80
|
+
|
|
81
|
+
`HALO_HUB_REPO='{{params.hub_repo}}' bash EXT models update`
|
|
82
|
+
|
|
83
|
+
The script prints one line per provider (`install` / `up-to-date` / `skip` /
|
|
84
|
+
`refuse` + reason), then a summary as its last line. By exit code:
|
|
85
|
+
- `0` — done. Relay the summary line; name any `refuse`d provider and its reason
|
|
86
|
+
(e.g. "needs a newer halo" → run `halo upgrade`). Takes effect immediately
|
|
87
|
+
— no restart, no page reload. Existing agents keep their own model / endpoint
|
|
88
|
+
(they live in each agent.yaml); only the choices offered change.
|
|
89
|
+
- `3` — nothing was written: a provider is new or its endpoints change. Show
|
|
90
|
+
the user the indented endpoint lines verbatim and ask whether to apply them.
|
|
91
|
+
Only after they agree, run the same command with `--yes` appended. Never add
|
|
92
|
+
`--yes` on your own.
|
|
93
|
+
- anything else — relay the error. Model configs come only from an https
|
|
94
|
+
GitHub / Gitea / Forgejo / GitLab hub with releases; a local / plain-git /
|
|
95
|
+
http hub is refused. "release API failed" / "no release for models" → same
|
|
96
|
+
hints as for `install`.
|
|
97
|
+
|
|
98
|
+
`bash EXT models list` shows each provider, which copy is in effect (`bundled`
|
|
99
|
+
= shipped with halo, `hub` = installed by this command) and its revision.
|
|
100
|
+
|
|
73
101
|
## Notes
|
|
74
102
|
|
|
75
103
|
- Extensions are global (all workspaces on this server). Say so if the user
|
|
@@ -4,6 +4,6 @@ description_zh: 安装 / 列出 / 删除管理后台的预览扩展。
|
|
|
4
4
|
|
|
5
5
|
params:
|
|
6
6
|
- key: hub_repo
|
|
7
|
-
description: "Hub that `install <id>`
|
|
8
|
-
description_zh: "`install <id>` 的来源仓库:GitHub / Gitea / Forgejo / GitLab 仓库地址(取最新的 `<id>-v*` release zip),GitHub 可简写为 `owner/repo`;也可以是任意 git 地址或本地仓库路径(取最新的 `<id>-v<x.y.z>`
|
|
7
|
+
description: "Hub that `install <id>` and `models` pull from: a GitHub / Gitea / Forgejo / GitLab repo URL (newest `<id>-v*` release zip), `owner/repo` for GitHub, or any git URL / local repo path (newest `<id>-v<x.y.z>` tag; not for `models`, which needs an https release hub). Empty = https://github.com/turmind/halo-hub."
|
|
8
|
+
description_zh: "`install <id>` 和 `models` 的来源仓库:GitHub / Gitea / Forgejo / GitLab 仓库地址(取最新的 `<id>-v*` release zip),GitHub 可简写为 `owner/repo`;也可以是任意 git 地址或本地仓库路径(取最新的 `<id>-v<x.y.z>` 标签;`models` 不支持,它只认 https 的 release 仓库)。留空 = https://github.com/turmind/halo-hub。"
|
|
9
9
|
default: https://github.com/turmind/halo-hub
|