openclacky 1.2.17 → 1.3.0
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 +34 -0
- data/lib/clacky/agent/skill_manager.rb +1 -1
- data/lib/clacky/agent/time_machine.rb +256 -74
- data/lib/clacky/agent/tool_executor.rb +12 -0
- data/lib/clacky/agent.rb +21 -31
- data/lib/clacky/agent_config.rb +18 -0
- data/lib/clacky/cli.rb +55 -3
- data/lib/clacky/default_skills/media-gen/SKILL.md +173 -5
- data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -0
- data/lib/clacky/media/base.rb +125 -0
- data/lib/clacky/media/dashscope.rb +243 -0
- data/lib/clacky/media/gemini.rb +10 -0
- data/lib/clacky/media/generator.rb +75 -0
- data/lib/clacky/media/openai_compat.rb +160 -0
- data/lib/clacky/message_history.rb +12 -7
- data/lib/clacky/providers.rb +28 -0
- data/lib/clacky/rich_ui_controller.rb +3 -1
- data/lib/clacky/server/backup_manager.rb +200 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -2
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +68 -15
- data/lib/clacky/server/channel/channel_manager.rb +180 -81
- data/lib/clacky/server/http_server.rb +348 -15
- data/lib/clacky/server/scheduler.rb +19 -0
- data/lib/clacky/server/session_registry.rb +8 -4
- data/lib/clacky/session_manager.rb +40 -2
- data/lib/clacky/skill.rb +3 -1
- data/lib/clacky/tools/trash_manager.rb +14 -0
- data/lib/clacky/ui2/components/command_suggestions.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +34 -7
- data/lib/clacky/ui2/ui_controller.rb +150 -19
- data/lib/clacky/utils/file_processor.rb +75 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +2038 -1147
- data/lib/clacky/web/app.js +22 -1
- data/lib/clacky/web/backup.js +119 -0
- data/lib/clacky/web/billing.js +94 -7
- data/lib/clacky/web/channels.js +81 -11
- data/lib/clacky/web/design-sample.css +247 -0
- data/lib/clacky/web/design-sample.html +127 -0
- data/lib/clacky/web/favicon.svg +16 -0
- data/lib/clacky/web/i18n.js +159 -31
- data/lib/clacky/web/index.html +175 -55
- data/lib/clacky/web/logo_nav_dark.png +0 -0
- data/lib/clacky/web/onboard.js +114 -28
- data/lib/clacky/web/sessions.js +436 -192
- data/lib/clacky/web/settings.js +21 -1
- data/lib/clacky/web/skills.js +6 -6
- data/lib/clacky/web/tasks.js +129 -61
- data/lib/clacky/web/utils.js +72 -0
- data/lib/clacky/web/ws-dispatcher.js +6 -0
- data/lib/clacky.rb +1 -0
- metadata +8 -3
- data/lib/clacky/server/channel/group_message_buffer.rb +0 -53
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "channel_ui_controller"
|
|
4
|
-
require_relative "group_message_buffer"
|
|
5
4
|
|
|
6
5
|
module Clacky
|
|
7
6
|
module Channel
|
|
@@ -28,14 +27,11 @@ module Clacky
|
|
|
28
27
|
# @param run_agent_task [Proc] (session_id, agent, &task) — from HttpServer
|
|
29
28
|
# @param interrupt_session [Proc] (session_id) — from HttpServer
|
|
30
29
|
# @param channel_config [Clacky::ChannelConfig]
|
|
31
|
-
# @param binding_mode [:
|
|
32
|
-
# :
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
# :chat — one session per chat (all users in a group share it).
|
|
37
|
-
# :user — one session per user (merges DMs and all groups).
|
|
38
|
-
def initialize(session_registry:, session_builder:, run_agent_task:, interrupt_session:, channel_config:, binding_mode: :chat_user)
|
|
30
|
+
# @param binding_mode [:chat | :chat_user | :user] how to map IM identities to sessions.
|
|
31
|
+
# :chat (default) — one session per chat (all users in a chat share it).
|
|
32
|
+
# :chat_user — one session per (chat, user) pair.
|
|
33
|
+
# :user — one session per user (merges all chats).
|
|
34
|
+
def initialize(session_registry:, session_builder:, run_agent_task:, interrupt_session:, channel_config:, binding_mode: :chat)
|
|
39
35
|
@registry = session_registry
|
|
40
36
|
@session_builder = session_builder
|
|
41
37
|
@run_agent_task = run_agent_task
|
|
@@ -46,8 +42,7 @@ module Clacky
|
|
|
46
42
|
@adapter_threads = []
|
|
47
43
|
@running = false
|
|
48
44
|
@mutex = Mutex.new
|
|
49
|
-
@session_counters = Hash.new(0)
|
|
50
|
-
@group_buffer = GroupMessageBuffer.new
|
|
45
|
+
@session_counters = Hash.new(0)
|
|
51
46
|
end
|
|
52
47
|
|
|
53
48
|
# Start all enabled adapters in background threads. Non-blocking.
|
|
@@ -82,15 +77,6 @@ module Clacky
|
|
|
82
77
|
@mutex.synchronize { @adapters.map(&:platform_id) }
|
|
83
78
|
end
|
|
84
79
|
|
|
85
|
-
# Return all buffered group chat messages for a given chat.
|
|
86
|
-
# @param chat_id [String]
|
|
87
|
-
# @return [Array<Hash>] each entry has :user_id, :user_name, :text
|
|
88
|
-
def group_history(chat_id)
|
|
89
|
-
@group_buffer.peek(chat_id).map do |e|
|
|
90
|
-
{ user_id: e.user_id, user_name: e.user_name, text: e.text }
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
80
|
# Proactively send a message to a user on the given platform.
|
|
95
81
|
#
|
|
96
82
|
# For Weixin (iLink protocol) a context_token is required for every outbound
|
|
@@ -244,7 +230,6 @@ module Clacky
|
|
|
244
230
|
|
|
245
231
|
def route_message(adapter, event)
|
|
246
232
|
if event[:observe_only]
|
|
247
|
-
@group_buffer.push(event[:chat_id], user_id: event[:user_id], user_name: event[:user_name], text: event[:text].to_s)
|
|
248
233
|
return
|
|
249
234
|
end
|
|
250
235
|
|
|
@@ -320,7 +305,7 @@ module Clacky
|
|
|
320
305
|
# Prepend buffered group history so the agent knows what was discussed
|
|
321
306
|
# before it was @-mentioned. Buffer is cleared atomically on take.
|
|
322
307
|
# WebUI always receives the raw user text — context is agent-only.
|
|
323
|
-
prompt = build_prompt_with_context(event
|
|
308
|
+
prompt = build_prompt_with_context(event, text)
|
|
324
309
|
|
|
325
310
|
# Start typing keepalive BEFORE sending any message.
|
|
326
311
|
# sendmessage cancels the typing indicator in WeChat protocol,
|
|
@@ -375,17 +360,28 @@ module Clacky
|
|
|
375
360
|
else
|
|
376
361
|
arg
|
|
377
362
|
end
|
|
378
|
-
unless session_id && @registry.
|
|
363
|
+
unless session_id && @registry.ensure(session_id)
|
|
379
364
|
adapter.send_text(chat_id, "Session not found. Use /list to see available sessions.")
|
|
380
365
|
return
|
|
381
366
|
end
|
|
382
367
|
|
|
383
368
|
# Detach channel_ui from the old session's web_ui, reattach to the new one.
|
|
369
|
+
# Also clear the old session's persisted agent.channel_info if it still
|
|
370
|
+
# matches this key — keeping channel_keys and channel_info strictly in sync
|
|
371
|
+
# so resolve_session never sees two sessions claim the same key via different
|
|
372
|
+
# sources (see comment in resolve_session).
|
|
384
373
|
old_session_id = resolve_session(event)
|
|
385
374
|
channel_ui = old_session_id ? channel_ui_for_session(old_session_id) : nil
|
|
386
375
|
|
|
387
376
|
if channel_ui
|
|
388
|
-
@registry.with_session(old_session_id)
|
|
377
|
+
@registry.with_session(old_session_id) do |s|
|
|
378
|
+
s[:ui]&.unsubscribe_channel(channel_ui)
|
|
379
|
+
s.delete(:channel_ui)
|
|
380
|
+
if s[:agent]&.respond_to?(:channel_info=) && s[:agent].respond_to?(:channel_info) &&
|
|
381
|
+
s[:agent].channel_info && channel_key_from_info(s[:agent].channel_info) == key
|
|
382
|
+
s[:agent].channel_info = nil
|
|
383
|
+
end
|
|
384
|
+
end
|
|
389
385
|
else
|
|
390
386
|
channel_ui = ChannelUIController.new(event, -> { adapter_for(event[:platform]) })
|
|
391
387
|
end
|
|
@@ -412,7 +408,17 @@ module Clacky
|
|
|
412
408
|
unbound = false
|
|
413
409
|
@registry.list.each do |summary|
|
|
414
410
|
@registry.with_session(summary[:id]) do |s|
|
|
415
|
-
|
|
411
|
+
if s[:channel_keys]&.delete(key)
|
|
412
|
+
unbound = true
|
|
413
|
+
# Keep agent.channel_info in sync with channel_keys (see resolve_session).
|
|
414
|
+
# Without this, after process restart + eviction, the fallback path would
|
|
415
|
+
# silently re-bind this key back to the unbinded session via stale
|
|
416
|
+
# channel_info, defeating /unbind.
|
|
417
|
+
if s[:agent]&.respond_to?(:channel_info=) && s[:agent].respond_to?(:channel_info) &&
|
|
418
|
+
s[:agent].channel_info && channel_key_from_info(s[:agent].channel_info) == key
|
|
419
|
+
s[:agent].channel_info = nil
|
|
420
|
+
end
|
|
421
|
+
end
|
|
416
422
|
end
|
|
417
423
|
end
|
|
418
424
|
adapter.send_text(chat_id, unbound ? "Unbound." : "No binding found.")
|
|
@@ -442,8 +448,10 @@ module Clacky
|
|
|
442
448
|
Commands:
|
|
443
449
|
? / h / help - show this help
|
|
444
450
|
/new / /clear - start a new session
|
|
445
|
-
/model - show current model &
|
|
446
|
-
/model <n> - switch
|
|
451
|
+
/model - show current model, cards & quick-switch list
|
|
452
|
+
/model <n> - switch card by number
|
|
453
|
+
/model s<n> - quick-switch model under current card
|
|
454
|
+
/model off - reset to card default
|
|
447
455
|
/skills - list available skills
|
|
448
456
|
/<skill> <args> - invoke a skill directly
|
|
449
457
|
/bind <n|session_id> - switch to a session (use /list to see numbers)
|
|
@@ -472,44 +480,126 @@ module Clacky
|
|
|
472
480
|
arg = text.sub(/\A\/model\s*/i, "").strip
|
|
473
481
|
|
|
474
482
|
if arg.empty?
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
483
|
+
show_model_list(adapter, chat_id, agent)
|
|
484
|
+
elsif arg =~ /\A\d+\z/
|
|
485
|
+
switch_model_by_index(adapter, chat_id, agent, arg.to_i - 1)
|
|
486
|
+
elsif arg =~ /\As(\d+)\z/i
|
|
487
|
+
switch_quick_by_index(adapter, chat_id, agent, $1.to_i - 1)
|
|
488
|
+
else
|
|
489
|
+
switch_model_by_name(adapter, chat_id, agent, arg)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def show_model_list(adapter, chat_id, agent)
|
|
494
|
+
info = agent.current_model_info
|
|
495
|
+
current = info&.dig(:model) || "unknown"
|
|
496
|
+
sub = info&.dig(:sub_model)
|
|
497
|
+
card = info&.dig(:card_model)
|
|
498
|
+
|
|
499
|
+
header = "Current: #{current}"
|
|
500
|
+
header += " (#{card})" if card && sub && sub != current
|
|
501
|
+
header += " (#{card})" if card && !sub
|
|
489
502
|
|
|
503
|
+
result = header
|
|
504
|
+
|
|
505
|
+
# Card list
|
|
506
|
+
models = agent.available_models
|
|
507
|
+
unless models.empty?
|
|
490
508
|
lines = models.each_with_index.map do |name, i|
|
|
491
509
|
marker = name == current ? " *" : ""
|
|
492
510
|
"#{i + 1}. #{name}#{marker}"
|
|
493
511
|
end
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
512
|
+
result += "\n\nCards (/model <n>):\n#{lines.join("\n")}"
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Quick-switch models under current provider
|
|
516
|
+
info = agent.current_model_info
|
|
517
|
+
provider_id = Clacky::Providers.find_by_base_url(info&.dig(:base_url))
|
|
518
|
+
if provider_id
|
|
519
|
+
quick = Clacky::Providers.models(provider_id)
|
|
520
|
+
unless quick.empty?
|
|
521
|
+
current_for_quick = sub || current
|
|
522
|
+
quick_lines = quick.each_with_index.map do |name, i|
|
|
523
|
+
marker = name == current_for_quick ? " *" : ""
|
|
524
|
+
" s#{i + 1}. #{name}#{marker}"
|
|
525
|
+
end
|
|
526
|
+
result += "\n\nQuick switch (/model s<n>):\n#{quick_lines.join("\n")}"
|
|
527
|
+
unless quick.include?(current_for_quick)
|
|
528
|
+
result += "\n(#{current_for_quick} not in this provider; switch card first)"
|
|
529
|
+
end
|
|
501
530
|
end
|
|
531
|
+
end
|
|
502
532
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
533
|
+
adapter.send_text(chat_id, result)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def switch_model_by_index(adapter, chat_id, agent, idx)
|
|
537
|
+
models = agent.config.models
|
|
538
|
+
if idx < 0 || idx >= models.length
|
|
539
|
+
adapter.send_text(chat_id, "Invalid number. Use /model to see available cards.")
|
|
540
|
+
return
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
model_id = models[idx]["id"]
|
|
544
|
+
if agent.switch_model_by_id(model_id)
|
|
545
|
+
new_info = agent.current_model_info
|
|
546
|
+
adapter.send_text(chat_id, "Switched to #{new_info&.dig(:model) || model_id}.")
|
|
510
547
|
else
|
|
511
|
-
adapter.send_text(chat_id, "
|
|
548
|
+
adapter.send_text(chat_id, "Failed to switch model.")
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def switch_quick_by_index(adapter, chat_id, agent, idx)
|
|
553
|
+
info = agent.current_model_info
|
|
554
|
+
provider_id = Clacky::Providers.find_by_base_url(info&.dig(:base_url))
|
|
555
|
+
|
|
556
|
+
unless provider_id
|
|
557
|
+
adapter.send_text(chat_id, "No quick-switch models. Use /model <n> to switch card.")
|
|
558
|
+
return
|
|
512
559
|
end
|
|
560
|
+
|
|
561
|
+
quick = Clacky::Providers.models(provider_id)
|
|
562
|
+
if idx < 0 || idx >= quick.length
|
|
563
|
+
adapter.send_text(chat_id, "Invalid s#{idx + 1}. Use /model to see quick-switch list.")
|
|
564
|
+
return
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
agent.set_session_sub_model(quick[idx])
|
|
568
|
+
new_info = agent.current_model_info
|
|
569
|
+
adapter.send_text(chat_id, "Switched to #{new_info&.dig(:sub_model) || new_info&.dig(:model)}.")
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def switch_model_by_name(adapter, chat_id, agent, name)
|
|
573
|
+
info = agent.current_model_info
|
|
574
|
+
provider_id = Clacky::Providers.find_by_base_url(info&.dig(:base_url))
|
|
575
|
+
|
|
576
|
+
unless provider_id
|
|
577
|
+
adapter.send_text(chat_id, "Current card has no quick-switch models. Use /model <n> to switch card.")
|
|
578
|
+
return
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
allowed = Clacky::Providers.models(provider_id)
|
|
582
|
+
if allowed.empty?
|
|
583
|
+
adapter.send_text(chat_id, "No quick-switch models available. Use /model <n> to switch card.")
|
|
584
|
+
return
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# Clear override
|
|
588
|
+
if name =~ /\A(off|clear|none)\z/i
|
|
589
|
+
agent.set_session_sub_model(nil)
|
|
590
|
+
new_info = agent.current_model_info
|
|
591
|
+
adapter.send_text(chat_id, "Back to card default (#{new_info&.dig(:model)}).")
|
|
592
|
+
return
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
unless allowed.include?(name)
|
|
596
|
+
adapter.send_text(chat_id, "'#{name}' not available. Use /model to see quick-switch list.")
|
|
597
|
+
return
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
agent.set_session_sub_model(name)
|
|
601
|
+
new_info = agent.current_model_info
|
|
602
|
+
adapter.send_text(chat_id, "Switched to #{new_info&.dig(:sub_model) || new_info&.dig(:model)}.")
|
|
513
603
|
end
|
|
514
604
|
|
|
515
605
|
def handle_skills_command(adapter, event)
|
|
@@ -546,12 +636,20 @@ module Clacky
|
|
|
546
636
|
|
|
547
637
|
def resolve_session(event)
|
|
548
638
|
key = channel_key(event)
|
|
639
|
+
|
|
640
|
+
# Resolve order per session:
|
|
641
|
+
# 1. explicit in-memory channel_keys (set by /bind or auto_create_session)
|
|
642
|
+
# 2. fallback to persisted agent.channel_info for evicted channel sessions
|
|
643
|
+
# (process restart with in-memory channel_keys lost)
|
|
644
|
+
#
|
|
645
|
+
# /bind and /unbind keep agent.channel_info strictly in sync with channel_keys
|
|
646
|
+
# (see handle_bind / handle_unbind), so the two sources never disagree on the
|
|
647
|
+
# same key for two different sessions — a single pass is sufficient.
|
|
549
648
|
@registry.list.each do |summary|
|
|
550
649
|
found = nil
|
|
551
650
|
@registry.with_session(summary[:id]) { |s| found = s[:channel_keys]&.include?(key) }
|
|
552
651
|
return summary[:id] if found
|
|
553
652
|
|
|
554
|
-
# Check evicted channel sessions via persisted channel_info
|
|
555
653
|
next unless summary[:source] == "channel"
|
|
556
654
|
next unless @registry.ensure(summary[:id])
|
|
557
655
|
agent = nil
|
|
@@ -561,6 +659,7 @@ module Clacky
|
|
|
561
659
|
bind_key_to_session(key, summary[:id])
|
|
562
660
|
return summary[:id]
|
|
563
661
|
end
|
|
662
|
+
|
|
564
663
|
nil
|
|
565
664
|
rescue StandardError => e
|
|
566
665
|
Clacky::Logger.error("[ChannelManager] Session resolve failed: #{e.message}")
|
|
@@ -655,7 +754,7 @@ module Clacky
|
|
|
655
754
|
case @binding_mode
|
|
656
755
|
when :chat then "#{platform}:chat:#{event[:chat_id]}"
|
|
657
756
|
when :user then "#{platform}:user:#{event[:user_id]}"
|
|
658
|
-
else # :chat_user
|
|
757
|
+
else # :chat_user
|
|
659
758
|
"#{platform}:chat:#{event[:chat_id]}:user:#{event[:user_id]}"
|
|
660
759
|
end
|
|
661
760
|
end
|
|
@@ -667,17 +766,16 @@ module Clacky
|
|
|
667
766
|
case @binding_mode
|
|
668
767
|
when :chat then "#{platform}:chat:#{chat_id}"
|
|
669
768
|
when :user then "#{platform}:user:#{user_id}"
|
|
670
|
-
else # :chat_user
|
|
769
|
+
else # :chat_user
|
|
671
770
|
"#{platform}:chat:#{chat_id}:user:#{user_id}"
|
|
672
771
|
end
|
|
673
772
|
end
|
|
674
773
|
|
|
675
774
|
private def extract_channel_info(event)
|
|
676
775
|
{
|
|
677
|
-
platform:
|
|
678
|
-
user_id:
|
|
679
|
-
|
|
680
|
-
chat_id: event[:chat_id]
|
|
776
|
+
platform: event[:platform],
|
|
777
|
+
user_id: event[:user_id],
|
|
778
|
+
chat_id: event[:chat_id]
|
|
681
779
|
}
|
|
682
780
|
end
|
|
683
781
|
|
|
@@ -721,12 +819,16 @@ module Clacky
|
|
|
721
819
|
|
|
722
820
|
key = channel_key_from_info(info)
|
|
723
821
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
822
|
+
# Arbitrate first: skip duplicate keys before attaching any channel_ui.
|
|
823
|
+
# Attaching channel_ui to a loser session would leave an orphan in its
|
|
824
|
+
# web_ui subscriber list (it cannot be detached later), which a subsequent
|
|
825
|
+
# /bind onto that session would then double up — causing duplicate broadcasts.
|
|
727
826
|
next unless bound_keys.add?(key)
|
|
728
827
|
bind_key_to_session(key, summary[:id])
|
|
729
828
|
|
|
829
|
+
event = { platform: info[:platform], chat_id: info[:chat_id] }
|
|
830
|
+
ensure_channel_ui_subscribed(summary[:id], event)
|
|
831
|
+
|
|
730
832
|
Clacky::Logger.info("[ChannelManager] Restored channel binding #{key} -> session #{summary[:id][0, 8]}")
|
|
731
833
|
restored_count += 1
|
|
732
834
|
end
|
|
@@ -739,23 +841,20 @@ module Clacky
|
|
|
739
841
|
Clacky::Logger.warn("[ChannelManager] Error stopping #{adapter.platform_id}: #{e.message}")
|
|
740
842
|
end
|
|
741
843
|
|
|
742
|
-
# Prepend
|
|
743
|
-
#
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return text if entries.empty?
|
|
748
|
-
|
|
749
|
-
total = @group_buffer.peek(chat_id).size
|
|
750
|
-
lines = entries.map { |e| "#{e.user_name || e.user_id}: #{e.text}" }.join("\n")
|
|
844
|
+
# Prepend sender identity and optional group chat history to the user's message.
|
|
845
|
+
# Returns the original text unchanged when there is no extra context.
|
|
846
|
+
private def build_prompt_with_context(event, text)
|
|
847
|
+
user_id = event[:user_id].to_s
|
|
848
|
+
sender = user_id
|
|
751
849
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
end
|
|
850
|
+
history = event[:group_history]
|
|
851
|
+
if history.nil? || history.empty?
|
|
852
|
+
return "[Sender: #{sender}]\n#{text}"
|
|
853
|
+
end
|
|
757
854
|
|
|
758
|
-
|
|
855
|
+
lines = history.map { |e| "#{e[:user_id]}: #{e[:text]}" }.join("\n")
|
|
856
|
+
header = "[Group chat history (#{history.size} messages)]"
|
|
857
|
+
[header, lines, "---", "[Sender: #{sender}]", text].join("\n")
|
|
759
858
|
end
|
|
760
859
|
end
|
|
761
860
|
end
|