openclacky 1.5.18 → 1.5.19
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/docs/session-observation-hooks.md +86 -0
- data/lib/clacky/agent/chunk_index.rb +4 -1
- data/lib/clacky/agent/history_navigation.rb +6 -1
- data/lib/clacky/agent/hook_manager.rb +11 -4
- data/lib/clacky/agent.rb +39 -3
- data/lib/clacky/billing/platform_billing.rb +9 -0
- data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +2 -1
- data/lib/clacky/default_extensions/advisor/hooks/advisor_complete.rb +3 -2
- data/lib/clacky/default_skills/channel-manager/SKILL.md +70 -9
- data/lib/clacky/default_skills/channel-manager/qq_setup.rb +79 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +45 -14
- data/lib/clacky/locales/en.rb +1 -1
- data/lib/clacky/locales/zh.rb +1 -1
- data/lib/clacky/media/openai_compat.rb +89 -2
- data/lib/clacky/message_format/bedrock.rb +5 -2
- data/lib/clacky/providers.rb +46 -27
- data/lib/clacky/server/channel/adapters/qq/adapter.rb +662 -0
- data/lib/clacky/server/channel/adapters/qq/api_client.rb +245 -0
- data/lib/clacky/server/channel/adapters/qq/gateway_client.rb +340 -0
- data/lib/clacky/server/channel/adapters/qq/qr_binder.rb +140 -0
- data/lib/clacky/server/channel/channel_config.rb +9 -0
- data/lib/clacky/server/channel/channel_ui_controller.rb +6 -2
- data/lib/clacky/server/channel.rb +1 -0
- data/lib/clacky/server/http_server.rb +116 -11
- data/lib/clacky/server/model_prices.rb +11 -3
- data/lib/clacky/shell_hook_loader.rb +3 -1
- data/lib/clacky/utils/model_pricing.rb +160 -20
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +357 -180
- data/lib/clacky/web/components/chat-navigator.js +154 -171
- data/lib/clacky/web/components/composer.js +11 -2
- data/lib/clacky/web/components/model-picker.js +133 -23
- data/lib/clacky/web/core/aside.js +72 -0
- data/lib/clacky/web/features/billing/store.js +1 -1
- data/lib/clacky/web/features/billing/view.js +126 -34
- data/lib/clacky/web/features/channels/store.js +25 -0
- data/lib/clacky/web/features/channels/view.js +122 -5
- data/lib/clacky/web/features/skills/view.js +12 -0
- data/lib/clacky/web/features/workspace/view.js +88 -12
- data/lib/clacky/web/i18n.js +64 -22
- data/lib/clacky/web/index.html +15 -1
- data/lib/clacky/web/projects.js +131 -3
- data/lib/clacky/web/sessions.js +65 -9
- data/lib/clacky/web/settings.js +19 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdaf1f7be658187d9515e3fcd1ca37a1ec8bdf04267b434a4d5722c3cbee3970
|
|
4
|
+
data.tar.gz: a7a62edc84b707407fd06b186627b1a705a8e9977b60b88453e23759dc43f4b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 574f85b7317b21b2d231f009405275a6aa77df9758bd2bb9d861a948d1bf35f32d876c1c154fb87f8248786490734aab84a6568ba522e37e840913216510f504
|
|
7
|
+
data.tar.gz: 6509de425a59612b5fad807c172affad49bee4da8b83d59ba9a204c3355814c1ef8f1b8ee233bee43755c624c3bbf4b9c7278378bf3b68e7faebcfc7096a6530
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.5.19] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Add the built-in QQ official-bot channel (#615 - @chengoak)
|
|
11
|
+
- Drag a session onto a project to move it (#610)
|
|
12
|
+
- Add bookmarks to the chat navigator and tighten its layout (#617)
|
|
13
|
+
- Scroll an overflowing session title on hover so it can be read in full (#618)
|
|
14
|
+
- Add session lifecycle hooks (#607)
|
|
15
|
+
- Rework the billing charts with a year heatmap and a 365-day range
|
|
16
|
+
- Add an "Add Model" button to the model picker
|
|
17
|
+
- Show an API Key hint in the model dialog and restyle the key link
|
|
18
|
+
- Add oc-glm-5.3 and oc-kimi-k3 aliases with pricing and capability caps
|
|
19
|
+
- Add abs-claude-sonnet-5-5 and abs-gpt-6.1-sol with corrected pricing
|
|
20
|
+
- Add Xiaomi MiMo V2.6 models (#614)
|
|
21
|
+
- Add the oc-minimax-m2.7 alias with its launch discount
|
|
22
|
+
- Show a GLM/Kimi series promotion with a badge in the model picker
|
|
23
|
+
- Forward Seedance reference images over the OpenAI-compatible video gateway (#616 - @codevoyager1984)
|
|
24
|
+
- Document the input image limits and the content_filter retry in the media skill (#593 - @codevoyager1984)
|
|
25
|
+
|
|
26
|
+
### Improved
|
|
27
|
+
- Make the model picker visually distinct from the variant switcher
|
|
28
|
+
- Explain model price discounts with a tooltip
|
|
29
|
+
- Soften the chat navigator tick color
|
|
30
|
+
- Hide the skills tab strip when only one tab is available
|
|
31
|
+
- Tune the billing chart axis labels
|
|
32
|
+
- Clarify ambient audio generation in the media skill (#606 - @YeEmrick)
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Stop the workspace file tree from flickering on reload
|
|
36
|
+
- Deliver web-originated replies to IM after a finished progress card (#603, #605 - @chengoak)
|
|
37
|
+
- Fall back to gem update when the OSS CDN fails (#583, #608 - @theluckystrike)
|
|
38
|
+
- Keep the chat position when toggling preview fullscreen (#613)
|
|
39
|
+
- Stop opening an empty variant panel for custom models
|
|
40
|
+
- Use a white background for the context bar popovers
|
|
41
|
+
- Point a rate limit error at switching models
|
|
42
|
+
- Skip reasoning fields for non-Anthropic Converse models
|
|
43
|
+
|
|
44
|
+
### More
|
|
45
|
+
- Rename the billing panel title to Usage
|
|
46
|
+
- Use 服务商 instead of 供应商 in model settings
|
|
47
|
+
|
|
7
48
|
## [1.5.18] - 2026-10-02
|
|
8
49
|
|
|
9
50
|
### Fixed
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Agent task lifecycle hooks
|
|
2
|
+
|
|
3
|
+
## Task lifecycle: on_start / on_complete
|
|
4
|
+
|
|
5
|
+
> Availability: the additional context and abnormal completion paths require
|
|
6
|
+
> the runtime change on branch `session-lifecycle-hooks`. They are not
|
|
7
|
+
> available in 1.5.16. Upgrade the runtime before relying on these fields.
|
|
8
|
+
|
|
9
|
+
Use the existing events without extra configuration. `on_start` runs after task
|
|
10
|
+
and history initialization, before execution, and retains `deny` / `handled`.
|
|
11
|
+
`on_complete` observes the outcome of each Agent turn, including normal completion,
|
|
12
|
+
waiting, handled starts, denial, exceptions and interruption. Its return value is
|
|
13
|
+
ignored, including `deny` and `handled`; all completion callbacks can run.
|
|
14
|
+
|
|
15
|
+
Ruby callbacks receive `(input, agent, context)` or `(result, agent, context)`.
|
|
16
|
+
The first two positions are unchanged; ordinary two-argument Ruby blocks ignore
|
|
17
|
+
the extra argument. Existing completion callbacks must check `result[:status] ==
|
|
18
|
+
:success` before doing success-only work. A handled result has `context[:handled]`
|
|
19
|
+
set to true; skip follow-up work already performed by the handler.
|
|
20
|
+
|
|
21
|
+
These are Agent-turn events for server, CLI and channel execution, not server
|
|
22
|
+
persistence notifications. Queued input emits start only when consumed; steering
|
|
23
|
+
inside an active turn does not create an extra start or completion. Automatic goal
|
|
24
|
+
continuations have their own turn events. Normal completion fires before goal
|
|
25
|
+
continuation, and exceptions still propagate after notification. A superseded
|
|
26
|
+
turn's exception cleanup cannot settle its successor.
|
|
27
|
+
|
|
28
|
+
Context contains `session_id`, `run_id` (process-local task ID, not durable),
|
|
29
|
+
`source` (execution source, not authenticated identity), `input_created_at` (the
|
|
30
|
+
optional timestamp passed to `run`), `observed_at` (UTC ISO8601), and `status`.
|
|
31
|
+
Start status is `running`. No input text is added to context; input and result
|
|
32
|
+
arguments can contain sensitive data.
|
|
33
|
+
|
|
34
|
+
| Completion context status | Meaning |
|
|
35
|
+
|---------------------------|---------|
|
|
36
|
+
| `completed` | Result explicitly has `status: :success`, without a feedback request. This does not certify App generation or validation. |
|
|
37
|
+
| `awaiting_user` | Successful result requests `awaiting_user_feedback`. |
|
|
38
|
+
| `failed` | Exception or non-success result. Structured exceptions can supply `error_code`. |
|
|
39
|
+
| `cancelled` | Interruption or denial. `reason` may be `user`, `replacement`, `hook_denied` or `tool_denied`. |
|
|
40
|
+
|
|
41
|
+
Cancelled completion notifications carry `result[:status] == :cancelled`, even
|
|
42
|
+
when the original denial return value remains unchanged for queue compatibility.
|
|
43
|
+
An execution start alone does not establish recovery from a previous failure.
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
id: session-observer
|
|
47
|
+
name: Session Observer
|
|
48
|
+
version: "0.1.0"
|
|
49
|
+
contributes:
|
|
50
|
+
hooks:
|
|
51
|
+
- event: on_start
|
|
52
|
+
file: hooks/start.rb
|
|
53
|
+
- event: on_complete
|
|
54
|
+
file: hooks/complete.rb
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# hooks/start.rb
|
|
59
|
+
Clacky::ExtensionHookRegistry.add do |_input, _agent, context|
|
|
60
|
+
next unless context
|
|
61
|
+
Clacky::Logger.debug("[session-observer] started", session_id: context[:session_id],
|
|
62
|
+
run_id: context[:run_id])
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# hooks/complete.rb
|
|
69
|
+
Clacky::ExtensionHookRegistry.add do |_result, _agent, context|
|
|
70
|
+
next unless context
|
|
71
|
+
Clacky::Logger.debug("[session-observer] settled", session_id: context[:session_id],
|
|
72
|
+
status: context[:status], run_id: context[:run_id])
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use separate files for separate registrations: each Ruby file is required only
|
|
78
|
+
once. Programmatic registration uses `agent.add_hook(:on_complete) { |result,
|
|
79
|
+
agent, context| ... }`. Shell hooks keep their JSON payload shape; neither the
|
|
80
|
+
Agent nor context is serialized, while abnormal completion results are now reported.
|
|
81
|
+
|
|
82
|
+
Callbacks execute synchronously; keep them short and dispatch network work
|
|
83
|
+
asynchronously. `StandardError` is logged without stopping later callbacks.
|
|
84
|
+
The host does not persist, retry, replay or acknowledge hook delivery. Consumers
|
|
85
|
+
must tolerate missing events and cannot infer inactivity solely from missing data.
|
|
86
|
+
`agent.emit_event(persist: true)` is a separate frontend-history feature.
|
|
@@ -41,6 +41,7 @@ module Clacky
|
|
|
41
41
|
nested = []
|
|
42
42
|
section_count = 0
|
|
43
43
|
user_start = nil
|
|
44
|
+
user_task_id = nil
|
|
44
45
|
archived_at = nil
|
|
45
46
|
front_matter = false
|
|
46
47
|
directory = File.dirname(path)
|
|
@@ -61,6 +62,7 @@ module Clacky
|
|
|
61
62
|
section_count += 1
|
|
62
63
|
nested << header[:nested_chunk] if header[:nested_chunk]
|
|
63
64
|
user_start = header[:role] == "user" ? start : nil
|
|
65
|
+
user_task_id = header[:role] == "user" ? header[:task_id] : nil
|
|
64
66
|
elsif user_start && !text.strip.empty?
|
|
65
67
|
# Match replay's empty/metadata-only user handling without
|
|
66
68
|
# retaining the rest of the user message or any assistant text.
|
|
@@ -69,8 +71,9 @@ module Clacky
|
|
|
69
71
|
next if visible.empty? && events.empty? && files.empty?
|
|
70
72
|
|
|
71
73
|
rounds.last[:length] = user_start - rounds.last[:start] if rounds.any?
|
|
72
|
-
rounds << { start: user_start }
|
|
74
|
+
rounds << { start: user_start, task_id: user_task_id }
|
|
73
75
|
user_start = nil
|
|
76
|
+
user_task_id = nil
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
rounds.last[:length] = position - rounds.last[:start] if rounds.any?
|
|
@@ -12,7 +12,12 @@ module Clacky
|
|
|
12
12
|
manifest = sources.map do |source|
|
|
13
13
|
entry = { key: source[:key], version: source[:version], count: navigation_source_count(source),
|
|
14
14
|
volatile: !!source[:rounds] || source[:continuation].to_a.any? }
|
|
15
|
-
|
|
15
|
+
if source[:rounds]
|
|
16
|
+
entry[:identities] = source[:rounds].map { |round| round[:user_msg].values_at(:created_at, :task_id) }
|
|
17
|
+
else
|
|
18
|
+
task_ids = source[:index][:rounds].map { |round| round[:task_id] }
|
|
19
|
+
entry[:task_ids] = task_ids if task_ids.any?
|
|
20
|
+
end
|
|
16
21
|
entry
|
|
17
22
|
end
|
|
18
23
|
{ sources: manifest, total: manifest.sum { |source| source[:count] } }
|
|
@@ -26,20 +26,27 @@ module Clacky
|
|
|
26
26
|
|
|
27
27
|
# @return [Hash] `{action: :allow}`, `{action: :deny, reason:}`, or
|
|
28
28
|
# `{action: :handled, result:}` when a hook fulfilled the event itself.
|
|
29
|
-
# on_start receives (user_input, agent) after task/history initialization.
|
|
29
|
+
# on_start receives (user_input, agent, context) after task/history initialization.
|
|
30
30
|
# Its handled result is returned unchanged from Agent#run; the extension
|
|
31
31
|
# supplies the run-result status and any queue/feedback flags it needs.
|
|
32
32
|
# on_start is not fired for steering input inside an already running turn.
|
|
33
|
-
#
|
|
33
|
+
# The agent follows the event payload, so ext hooks can
|
|
34
34
|
# call `agent.emit_event(...)`. Blocks are procs — those declaring fewer
|
|
35
|
-
# params (`|call|`, `|call, result|`) silently ignore
|
|
35
|
+
# params (`|call|`, `|call, result|`) silently ignore extra arguments.
|
|
36
|
+
# Start/complete append context after the agent. Completion is observation-only.
|
|
36
37
|
def trigger(event, *args)
|
|
37
38
|
validate_event!(event)
|
|
39
|
+
# Keep payload hashes positional: Ruby 2.6 otherwise promotes their keys
|
|
40
|
+
# into keyword arguments, breaking existing tool hooks.
|
|
41
|
+
context = args.pop if [:on_start, :on_complete].include?(event) && args.length == 2
|
|
38
42
|
result = { action: :allow }
|
|
39
43
|
|
|
40
44
|
@hooks[event].each do |hook|
|
|
41
45
|
begin
|
|
42
|
-
|
|
46
|
+
callback_args = [*args, @agent]
|
|
47
|
+
callback_args << context.dup if context
|
|
48
|
+
hook_result = hook.call(*callback_args)
|
|
49
|
+
next if event == :on_complete
|
|
43
50
|
next unless hook_result.is_a?(Hash)
|
|
44
51
|
# First deny wins and stops the chain: a weaker later verdict must
|
|
45
52
|
# never clobber a stronger earlier one, and the first deny's reason
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -490,6 +490,8 @@ module Clacky
|
|
|
490
490
|
# method-level and must be able to reference them on any exit path.
|
|
491
491
|
result = nil
|
|
492
492
|
run_turn_started = false
|
|
493
|
+
completion_sent = false
|
|
494
|
+
run_context = nil
|
|
493
495
|
|
|
494
496
|
# Intercept /goal ... commands before any task/LLM work. Control-plane
|
|
495
497
|
# commands (status/pause/resume/clear) return immediately without a turn;
|
|
@@ -520,6 +522,9 @@ module Clacky
|
|
|
520
522
|
|
|
521
523
|
# Start new task for Time Machine
|
|
522
524
|
task_id = start_new_task(title: display_text.to_s.empty? ? user_input.to_s : display_text.to_s)
|
|
525
|
+
run_context = { session_id: @session_id, run_id: task_id.to_s,
|
|
526
|
+
source: @source.to_s, input_created_at: created_at,
|
|
527
|
+
observed_at: Time.now.utc.iso8601(6), status: "running" }
|
|
523
528
|
|
|
524
529
|
# Continuation of a previously-interrupted task (e.g. user sent a
|
|
525
530
|
# supplementary message without stopping the running task) keeps the
|
|
@@ -589,13 +594,15 @@ module Clacky
|
|
|
589
594
|
@input_mutex.synchronize { @accepting_steering = true }
|
|
590
595
|
notify_input_queue
|
|
591
596
|
# The task and user history already exist. A terminal verdict skips the
|
|
592
|
-
# loop
|
|
593
|
-
hook_result = @hooks.trigger(:on_start, user_input)
|
|
597
|
+
# loop; ensure still reports completion and cleans up this started turn.
|
|
598
|
+
hook_result = @hooks.trigger(:on_start, user_input, run_context)
|
|
594
599
|
case hook_result[:action]
|
|
595
600
|
when :deny
|
|
601
|
+
run_context[:reason] = "hook_denied"
|
|
596
602
|
@ui&.show_warning(hook_result[:reason] || "Task denied by hook")
|
|
597
603
|
return result = build_result.merge(queue_paused: true)
|
|
598
604
|
when :handled
|
|
605
|
+
run_context[:handled] = true
|
|
599
606
|
return result = hook_result[:result]
|
|
600
607
|
end
|
|
601
608
|
|
|
@@ -793,6 +800,7 @@ module Clacky
|
|
|
793
800
|
notify_input_queue
|
|
794
801
|
result = build_result(awaiting_user_feedback: awaiting_user_feedback)
|
|
795
802
|
result[:queue_paused] = true if awaiting_user_feedback || task_interrupted
|
|
803
|
+
run_context[:reason] = "tool_denied" if task_interrupted
|
|
796
804
|
|
|
797
805
|
# Run skill evolution hooks after main loop completes
|
|
798
806
|
# Skip if task was interrupted by user (denied tool) or awaiting user feedback
|
|
@@ -825,7 +833,8 @@ module Clacky
|
|
|
825
833
|
awaiting_user_feedback: awaiting_user_feedback
|
|
826
834
|
)
|
|
827
835
|
end
|
|
828
|
-
|
|
836
|
+
completion_sent = true
|
|
837
|
+
complete_run_hook(result, run_context)
|
|
829
838
|
|
|
830
839
|
# Standing-goal loop: after a completed turn, ask the judge whether the
|
|
831
840
|
# goal is met. If not (and budget/health allow), auto-run the next turn
|
|
@@ -840,6 +849,8 @@ module Clacky
|
|
|
840
849
|
result[:queue_paused] = true if @goal_manager&.state&.paused?
|
|
841
850
|
result
|
|
842
851
|
rescue Clacky::AgentInterrupted
|
|
852
|
+
result = { status: :cancelled }
|
|
853
|
+
run_context[:reason] = (Thread.current[:interrupt_reason] || :user).to_s if run_context
|
|
843
854
|
# A cancelled fan-out captured its subagents' progress but never reached
|
|
844
855
|
# observe() to persist it — anchor those trails now so a page reload
|
|
845
856
|
# after the interrupt still shows what the subagents did.
|
|
@@ -852,6 +863,7 @@ module Clacky
|
|
|
852
863
|
# Let CLI handle the interrupt message
|
|
853
864
|
raise
|
|
854
865
|
rescue StandardError => e
|
|
866
|
+
run_context[:error_code] = e.error_code if run_context && e.respond_to?(:error_code)
|
|
855
867
|
# Log complete error information to debug_logs for troubleshooting
|
|
856
868
|
@debug_logs << {
|
|
857
869
|
timestamp: Time.now.iso8601,
|
|
@@ -871,6 +883,11 @@ module Clacky
|
|
|
871
883
|
result = build_result(:error, error: e.message)
|
|
872
884
|
raise
|
|
873
885
|
ensure
|
|
886
|
+
# A recursive goal continuation owns its own completion. Do not let an
|
|
887
|
+
# unwinding predecessor settle a newer turn or report the same turn twice.
|
|
888
|
+
if run_context && !completion_sent && task_id == @current_task_id
|
|
889
|
+
complete_run_hook(result, run_context)
|
|
890
|
+
end
|
|
874
891
|
if run_turn_started && task_id == @current_task_id
|
|
875
892
|
@input_mutex.synchronize do
|
|
876
893
|
@accepting_steering = false
|
|
@@ -888,6 +905,25 @@ module Clacky
|
|
|
888
905
|
Clacky::Telemetry.task!(result: result) if run_turn_started
|
|
889
906
|
end
|
|
890
907
|
|
|
908
|
+
private def complete_run_hook(result, context)
|
|
909
|
+
result = { status: :error } unless result.is_a?(Hash)
|
|
910
|
+
cancelled = context[:reason] || [:cancelled, :interrupted].include?(result[:status])
|
|
911
|
+
status = if cancelled
|
|
912
|
+
"cancelled"
|
|
913
|
+
elsif result[:status] != :success
|
|
914
|
+
"failed"
|
|
915
|
+
elsif result[:awaiting_user_feedback]
|
|
916
|
+
"awaiting_user"
|
|
917
|
+
else
|
|
918
|
+
"completed"
|
|
919
|
+
end
|
|
920
|
+
# Denied work can retain its historical return value for queue control,
|
|
921
|
+
# but completion observers must never interpret it as a successful run.
|
|
922
|
+
notification = cancelled ? result.merge(status: :cancelled) : result
|
|
923
|
+
@hooks.trigger(:on_complete, notification,
|
|
924
|
+
context.merge(status: status, observed_at: Time.now.utc.iso8601(6)))
|
|
925
|
+
end
|
|
926
|
+
|
|
891
927
|
# Shared by initial input and steering; only the execution thread writes history.
|
|
892
928
|
private def append_user_input(user_input, files: nil, reference_contexts: nil,
|
|
893
929
|
display_text: nil, created_at: nil, references_display: nil,
|
|
@@ -24,6 +24,7 @@ module Clacky
|
|
|
24
24
|
"dsk-deepseek-v4-flash-vision-exp" => "deepseek-v4-flash-vision-exp",
|
|
25
25
|
# gpt via bedrock (abs-, OpenAI-compatible endpoint)
|
|
26
26
|
"abs-gpt-6-astra" => "global.openai.gpt-6-astra",
|
|
27
|
+
"abs-gpt-6.1-sol" => "global.openai.gpt-6.1-sol",
|
|
27
28
|
"abs-gpt-6-sol" => "global.openai.gpt-6-sol",
|
|
28
29
|
"abs-gpt-6-luna" => "global.openai.gpt-6-luna",
|
|
29
30
|
"abs-gpt-5.6-sol" => "global.openai.gpt-5.6-sol",
|
|
@@ -37,10 +38,18 @@ module Clacky
|
|
|
37
38
|
"abs-claude-opus-4-8" => "global.anthropic.claude-opus-4-8",
|
|
38
39
|
"abs-claude-opus-4-7" => "global.anthropic.claude-opus-4-7",
|
|
39
40
|
"abs-claude-opus-4-6" => "global.anthropic.claude-opus-4-6",
|
|
41
|
+
"abs-claude-sonnet-5-5" => "global.anthropic.claude-sonnet-5-5",
|
|
40
42
|
"abs-claude-sonnet-5" => "global.anthropic.claude-sonnet-5",
|
|
41
43
|
"abs-claude-sonnet-4-6" => "global.anthropic.claude-sonnet-4-6",
|
|
42
44
|
"abs-claude-sonnet-4-5" => "global.anthropic.claude-sonnet-4-5",
|
|
43
45
|
"abs-claude-haiku-4-5" => "global.anthropic.claude-haiku-4-5",
|
|
46
|
+
# GLM / Kimi / MiniMax via TokHub (oc-). The gateway books these under
|
|
47
|
+
# the alias itself, so the mapping is an identity — it exists so the
|
|
48
|
+
# billing UI can resolve the alias instead of dropping the record as
|
|
49
|
+
# unknown.
|
|
50
|
+
"oc-glm-5.3" => "oc-glm-5.3",
|
|
51
|
+
"oc-kimi-k3" => "oc-kimi-k3",
|
|
52
|
+
"oc-minimax-m2.7" => "oc-minimax-m2.7",
|
|
44
53
|
# gemini chat (or-)
|
|
45
54
|
"or-gemini-3-1-pro" => "gemini-3.1-pro-preview",
|
|
46
55
|
"or-gemini-3-8-flash" => "gemini-3.8-flash",
|
|
@@ -127,7 +127,7 @@ module Clacky
|
|
|
127
127
|
# rounds with no tool calls at all (e.g. the very first "hi"). The
|
|
128
128
|
# pending event is emitted synchronously so the UI can show a "working"
|
|
129
129
|
# state immediately instead of the card popping in out of nowhere.
|
|
130
|
-
def finish_run
|
|
130
|
+
def finish_run(analyze: true)
|
|
131
131
|
snapshot = @mutex.synchronize do
|
|
132
132
|
snap = {
|
|
133
133
|
tools: @tools_this_run,
|
|
@@ -139,6 +139,7 @@ module Clacky
|
|
|
139
139
|
@tools_this_run = 0
|
|
140
140
|
snap
|
|
141
141
|
end
|
|
142
|
+
return unless analyze
|
|
142
143
|
Clacky::Logger.info("[Advisor] finish_run",
|
|
143
144
|
session: @agent.session_id.to_s,
|
|
144
145
|
tools: snapshot[:tools],
|
|
@@ -4,8 +4,9 @@ require_relative "advisor"
|
|
|
4
4
|
|
|
5
5
|
# on_complete: a run has ended — reset the per-run analysis budget so the
|
|
6
6
|
# next run starts fresh.
|
|
7
|
-
Clacky::ExtensionHookRegistry.add do |
|
|
7
|
+
Clacky::ExtensionHookRegistry.add do |result, agent, context|
|
|
8
8
|
next unless Clacky::Advisor.enabled_for?(agent)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
successful = result.is_a?(Hash) && result[:status] == :success && !context&.dig(:handled)
|
|
11
|
+
Clacky::Advisor.worker_for(agent).finish_run(analyze: successful)
|
|
11
12
|
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: channel-manager
|
|
3
3
|
name_zh: 渠道管理
|
|
4
|
-
description_zh: 接入飞书、企业微信、微信、钉钉、Discord、Telegram 等 IM 渠道,支持配置、启停、诊断和发消息。
|
|
4
|
+
description_zh: 接入飞书、企业微信、微信、钉钉、Discord、Telegram、QQ 等 IM 渠道,支持配置、启停、诊断和发消息。
|
|
5
5
|
description: |
|
|
6
|
-
Configure IM platform channels (Feishu, WeCom, Weixin, Discord, Telegram, DingTalk) for openclacky.
|
|
6
|
+
Configure IM platform channels (Feishu, WeCom, Weixin, Discord, Telegram, DingTalk, QQ) for openclacky.
|
|
7
7
|
Uses browser automation for navigation; guides the user to paste credentials and perform UI steps.
|
|
8
|
-
Trigger on: "channel setup", "setup feishu", "setup wecom", "setup weixin", "setup wechat", "setup discord", "setup telegram", "setup dingtalk",
|
|
8
|
+
Trigger on: "channel setup", "setup feishu", "setup wecom", "setup weixin", "setup wechat", "setup discord", "setup telegram", "setup dingtalk", "setup qq",
|
|
9
9
|
"channel config", "channel status", "channel enable", "channel disable", "channel reconfigure", "channel doctor",
|
|
10
|
-
"send message to weixin", "send message to feishu", "send message to wecom", "send message to discord", "send message to telegram", "send message to dingtalk".
|
|
10
|
+
"send message to weixin", "send message to feishu", "send message to wecom", "send message to discord", "send message to telegram", "send message to dingtalk", "send message to qq".
|
|
11
11
|
Subcommands: setup, status, enable <platform>, disable <platform>, reconfigure, doctor, send.
|
|
12
12
|
argument-hint: "setup | status | enable <platform> | disable <platform> | reconfigure | doctor | send <platform> <message>"
|
|
13
13
|
allowed-tools:
|
|
@@ -30,13 +30,13 @@ Configure IM platform channels for openclacky.
|
|
|
30
30
|
|
|
31
31
|
| User says | Subcommand |
|
|
32
32
|
|---|---|
|
|
33
|
-
| `channel setup`, `setup feishu`, `setup wecom`, `setup weixin`, `setup wechat`, `setup discord`, `setup telegram`, `setup dingtalk` | setup |
|
|
33
|
+
| `channel setup`, `setup feishu`, `setup wecom`, `setup weixin`, `setup wechat`, `setup discord`, `setup telegram`, `setup dingtalk`, `setup qq` | setup |
|
|
34
34
|
| `channel status` | status |
|
|
35
|
-
| `channel enable feishu/wecom/weixin/discord/telegram/dingtalk` | enable |
|
|
36
|
-
| `channel disable feishu/wecom/weixin/discord/telegram/dingtalk` | disable |
|
|
35
|
+
| `channel enable feishu/wecom/weixin/discord/telegram/dingtalk/qq` | enable |
|
|
36
|
+
| `channel disable feishu/wecom/weixin/discord/telegram/dingtalk/qq` | disable |
|
|
37
37
|
| `channel reconfigure` | reconfigure |
|
|
38
38
|
| `channel doctor` | doctor |
|
|
39
|
-
| `send <message> to weixin/feishu/wecom/discord/telegram/dingtalk` | send |
|
|
39
|
+
| `send <message> to weixin/feishu/wecom/discord/telegram/dingtalk/qq` | send |
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
@@ -80,6 +80,7 @@ dingtalk ✅ yes ✅ yes client_id: ding_xxx...
|
|
|
80
80
|
- Discord: show `has_token: true/false` (token value is never displayed)
|
|
81
81
|
- Telegram: show `has_token: true/false` (bot token is never displayed)
|
|
82
82
|
- DingTalk: show `client_id` (truncated to 12 chars)
|
|
83
|
+
- QQ: show `app_id` (truncated to 12 chars) and `sandbox: true/false`
|
|
83
84
|
|
|
84
85
|
If the API is unreachable or returns an empty list: "No channels configured yet. Run `/channel-manager setup` to get started."
|
|
85
86
|
|
|
@@ -96,6 +97,7 @@ Ask:
|
|
|
96
97
|
> 4. Discord
|
|
97
98
|
> 5. Telegram (Bot API)
|
|
98
99
|
> 6. DingTalk
|
|
100
|
+
> 7. QQ (official bot, C2C & group @)
|
|
99
101
|
|
|
100
102
|
---
|
|
101
103
|
|
|
@@ -391,6 +393,56 @@ On success:
|
|
|
391
393
|
|
|
392
394
|
---
|
|
393
395
|
|
|
396
|
+
### QQ setup (official QQ bot)
|
|
397
|
+
|
|
398
|
+
QQ bots are created on the QQ Open Platform (q.qq.com). The user registers a bot, enables the C2C / group-message scopes, and pastes the App ID / App Secret here. The adapter connects over the QQ Gateway WebSocket; no browser automation is required from this skill.
|
|
399
|
+
|
|
400
|
+
#### Step 1 — Create the bot on the QQ Open Platform
|
|
401
|
+
|
|
402
|
+
Tell the user (localize if they are chatting in Chinese):
|
|
403
|
+
|
|
404
|
+
> 1. Open the QQ Open Platform: https://q.qq.com (sandbox testing: use the sandbox toggle / sandbox AppID in the bot admin console).
|
|
405
|
+
> 2. Register / log in and create a robot (机器人). After creation you will find **AppID** and **AppSecret** in the bot's "开发设置 / Development Settings" page.
|
|
406
|
+
> 3. In "功能配置 / 配置" (or the sandbox panel), make sure the bot has the private-chat (C2C) and group @ message permissions enabled; add yourself as a sandbox tester if you are testing before release.
|
|
407
|
+
> 4. Paste the credentials back here in one line:
|
|
408
|
+
>
|
|
409
|
+
> `app_id=YOUR_APPID app_secret=YOUR_APPSECRET sandbox=true_or_false`
|
|
410
|
+
|
|
411
|
+
Wait for the reply. Parse with tolerant regex (`app_id=\S+`, `app_secret=\S+`, `sandbox=(true|false)`; default `sandbox=false` when omitted).
|
|
412
|
+
|
|
413
|
+
Optionally validate the credentials before saving (best-effort; requires outbound access to api.bot.qq.com). The token endpoint is shared by production and sandbox — only the openapi host differs — so validation takes no `--sandbox` flag:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
ruby "SKILL_DIR/qq_setup.rb" --validate "<APP_ID> <APP_SECRET>"
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### Step 2 — Save credentials
|
|
420
|
+
|
|
421
|
+
The server persists the credentials and immediately opens the gateway WebSocket:
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/qq \
|
|
425
|
+
-H "Content-Type: application/json" \
|
|
426
|
+
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>","sandbox":<TRUE_OR_FALSE>}'
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- `200` — credentials saved; the QQ gateway session starts.
|
|
430
|
+
- `4xx` — show the returned error (bad AppID/Secret, or permission not granted) and offer to retry.
|
|
431
|
+
|
|
432
|
+
#### Step 3 — Verify
|
|
433
|
+
|
|
434
|
+
Tell the user:
|
|
435
|
+
|
|
436
|
+
> ✅ QQ channel configured. Send your bot a private message, or @ it in a group.
|
|
437
|
+
>
|
|
438
|
+
> Notes:
|
|
439
|
+
> - Before release, only accounts added as sandbox testers can reach the bot.
|
|
440
|
+
> - QQ bots reply in the passive window (5 minutes after the user's message); very long tasks may reply after the window closes.
|
|
441
|
+
> - Channel (频道 / guild) messages are parsed but outbound replies are not supported yet — use C2C or groups.
|
|
442
|
+
> - Optional: restrict who can use the bot via the `allowed_users` list (QQ openid values).
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
394
446
|
## `enable`
|
|
395
447
|
|
|
396
448
|
Call the server API to re-enable the platform (this reads from disk, sets enabled, saves, and hot-reloads):
|
|
@@ -466,6 +518,7 @@ Check each item, report ✅ / ❌ with remediation:
|
|
|
466
518
|
- Weixin: `token` present and non-empty in `channels.yml`
|
|
467
519
|
- Discord: `bot_token` present and non-empty in `channels.yml`
|
|
468
520
|
- Telegram: `bot_token` present and non-empty
|
|
521
|
+
- QQ: `app_id`, `app_secret` present and non-empty
|
|
469
522
|
3. **Feishu credentials** (if enabled) — run the token API call, check `code=0`.
|
|
470
523
|
4. **Weixin token** (if enabled) — call `GET /api/channels` and check `has_token: true` for the weixin entry.
|
|
471
524
|
5. **Telegram credentials** (if enabled) — call `getMe` against the Bot API:
|
|
@@ -495,6 +548,13 @@ Check each item, report ✅ / ❌ with remediation:
|
|
|
495
548
|
```
|
|
496
549
|
- `WebSocket connected` → ✅
|
|
497
550
|
- `Stream endpoint error` or `token error` → ❌ "DingTalk credentials invalid — re-run setup"
|
|
551
|
+
8. **QQ credentials** (if enabled) — search today's log:
|
|
552
|
+
```bash
|
|
553
|
+
grep -iE "QQ gateway|qq gateway|gateway ready|AppID|app_id.*invalid" \
|
|
554
|
+
~/.clacky/logger/clacky-$(date +%Y-%m-%d).log
|
|
555
|
+
```
|
|
556
|
+
- gateway ready / identified → ✅
|
|
557
|
+
- invalid app_id / app_secret or identify failed → ❌ "QQ credentials invalid — re-run setup"
|
|
498
558
|
|
|
499
559
|
---
|
|
500
560
|
|
|
@@ -505,7 +565,7 @@ Proactively send a message to a user via an IM channel adapter.
|
|
|
505
565
|
### Parse the request
|
|
506
566
|
|
|
507
567
|
Extract two things from the user's instruction:
|
|
508
|
-
- **platform** — one of `weixin`, `feishu`, `wecom`, `discord`, `telegram`, `dingtalk`
|
|
568
|
+
- **platform** — one of `weixin`, `feishu`, `wecom`, `discord`, `telegram`, `dingtalk`, `qq`
|
|
509
569
|
- **message** — the text content to send
|
|
510
570
|
|
|
511
571
|
If the platform cannot be inferred, ask the user to clarify.
|
|
@@ -545,6 +605,7 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/
|
|
|
545
605
|
### Constraints & notes
|
|
546
606
|
|
|
547
607
|
- **Weixin (iLink protocol)**: Every outbound message requires a `context_token` that is obtained from the most recent inbound message from that user. The token is cached in memory and reset on server restart. If the server was restarted since the user last wrote, the token is gone and the send will fail — the user must message the bot again.
|
|
608
|
+
- **QQ**: C2C/group replies reuse the recent inbound `msg_id` (passive reply window, ~5 minutes); proactive sends outside that window may be rejected.
|
|
548
609
|
- **Feishu / WeCom / Discord / Telegram**: No per-message token required. As long as the adapter is running and the `user_id` / `chat_id` (or Discord channel/user id) is valid, the message will be delivered. For Telegram specifically, the `user_id` must be a Telegram chat_id that the bot can write to — the user must have sent at least one message to the bot first.
|
|
549
610
|
- This feature is intended for **proactive notifications** (e.g. task completion, reminders). It is not a replacement for the normal reply flow triggered by inbound messages.
|
|
550
611
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# QQ channel setup helper for the channel-manager skill.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# qq_setup.rb --validate "<APP_ID> <APP_SECRET>" [--sandbox]
|
|
8
|
+
#
|
|
9
|
+
# Exchanges the AppID/AppSecret for a bot access_token via the QQ Open
|
|
10
|
+
# Platform, so the skill can validate credentials BEFORE posting them to the
|
|
11
|
+
# server API. Prints a small JSON document on success; exits non-zero with an
|
|
12
|
+
# error message on failure. Uses only the Ruby standard library.
|
|
13
|
+
|
|
14
|
+
require "json"
|
|
15
|
+
require "net/http"
|
|
16
|
+
require "uri"
|
|
17
|
+
|
|
18
|
+
PORTAL_URL = "https://q.qq.com"
|
|
19
|
+
# The token endpoint is shared by production and sandbox ("不区分正式环境、沙箱环境");
|
|
20
|
+
# only the openapi host differs.
|
|
21
|
+
TOKEN_URL = "https://api.bot.qq.com/app/getAppAccessToken"
|
|
22
|
+
|
|
23
|
+
def fail!(message)
|
|
24
|
+
warn({ "ok" => false, "error" => message }.to_json)
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def print_usage
|
|
29
|
+
warn "Usage: #{$PROGRAM_NAME} --validate \"<APP_ID> <APP_SECRET>\" [--sandbox]"
|
|
30
|
+
warn " #{$PROGRAM_NAME} --portal-url"
|
|
31
|
+
exit 2
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_pair(pair)
|
|
35
|
+
app_id, app_secret = pair.to_s.split(/\s+/, 2)
|
|
36
|
+
fail!("missing app_id or app_secret") if app_id.nil? || app_id.empty? || app_secret.nil? || app_secret.empty?
|
|
37
|
+
[app_id, app_secret]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def request_token(app_id, app_secret)
|
|
41
|
+
uri = URI(TOKEN_URL)
|
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
43
|
+
http.use_ssl = true
|
|
44
|
+
http.open_timeout = 10
|
|
45
|
+
http.read_timeout = 10
|
|
46
|
+
|
|
47
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
48
|
+
req.body = { appId: app_id, clientSecret: app_secret }.to_json
|
|
49
|
+
http.request(req)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_body(res)
|
|
53
|
+
JSON.parse(res.body)
|
|
54
|
+
rescue JSON::ParserError
|
|
55
|
+
fail!("non-JSON response (HTTP #{res.code})")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate!(pair, sandbox: false)
|
|
59
|
+
app_id, app_secret = parse_pair(pair)
|
|
60
|
+
res = request_token(app_id, app_secret)
|
|
61
|
+
data = parse_body(res)
|
|
62
|
+
if res.is_a?(Net::HTTPSuccess) && data["access_token"]
|
|
63
|
+
puts({ "ok" => true, "app_id" => app_id, "sandbox" => sandbox, "expires_in" => data["expires_in"] }.to_json)
|
|
64
|
+
else
|
|
65
|
+
fail!(data["message"] || data["msg"] || "token request failed (HTTP #{res.code})")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
mode = ARGV.first
|
|
70
|
+
print_usage if mode.nil?
|
|
71
|
+
|
|
72
|
+
case mode
|
|
73
|
+
when "--portal-url"
|
|
74
|
+
puts PORTAL_URL
|
|
75
|
+
when "--validate"
|
|
76
|
+
validate!(ARGV[1], sandbox: ARGV.include?("--sandbox"))
|
|
77
|
+
else
|
|
78
|
+
print_usage
|
|
79
|
+
end
|