fp-core 0.1.0__py3-none-any.whl
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.
- fp_core/__init__.py +21 -0
- fp_core/commands/__init__.py +132 -0
- fp_core/commands/back.py +73 -0
- fp_core/commands/clear.py +14 -0
- fp_core/commands/compact.py +10 -0
- fp_core/commands/exit_bang.py +27 -0
- fp_core/commands/exit_cmd.py +9 -0
- fp_core/commands/fork.py +16 -0
- fp_core/commands/help.py +25 -0
- fp_core/commands/history.py +32 -0
- fp_core/commands/memory_cmd.py +30 -0
- fp_core/commands/model.py +24 -0
- fp_core/commands/resume.py +166 -0
- fp_core/commands/session.py +13 -0
- fp_core/config.py +329 -0
- fp_core/core/__init__.py +0 -0
- fp_core/core/agent.py +762 -0
- fp_core/core/conversation.py +355 -0
- fp_core/core/io.py +180 -0
- fp_core/core/lifecycle.py +342 -0
- fp_core/core/llm_client.py +228 -0
- fp_core/core/llm_service.py +109 -0
- fp_core/core/prompt_builder.py +123 -0
- fp_core/core/session.py +369 -0
- fp_core/core/tool_executor.py +59 -0
- fp_core/display.py +366 -0
- fp_core/plugins/base/__init__.py +0 -0
- fp_core/plugins/base/plugin.py +273 -0
- fp_core/prompts/__init__.py +5 -0
- fp_core/prompts/agent.py +29 -0
- fp_core/skills/__init__.py +5 -0
- fp_core/skills/loader.py +209 -0
- fp_core/tools/__init__.py +216 -0
- fp_core/tools/core.py +239 -0
- fp_core/tools/plugins/__init__.py +0 -0
- fp_core/tools/plugins/memory_read_plugin.py +138 -0
- fp_core/tools/plugins/memory_save_plugin.py +88 -0
- fp_core/tools/plugins/python_plugin.py +106 -0
- fp_core/tools/plugins/subagent_plugin.py +320 -0
- fp_core/tools/plugins/task_clear_plugin.py +59 -0
- fp_core/tools/plugins/task_create_plugin.py +73 -0
- fp_core/tools/plugins/task_list_plugin.py +68 -0
- fp_core/tools/plugins/task_update_plugin.py +78 -0
- fp_core/tools/plugins/web_fetch_plugin.py +71 -0
- fp_core/tools/plugins/web_search_plugin.py +255 -0
- fp_core-0.1.0.dist-info/METADATA +188 -0
- fp_core-0.1.0.dist-info/RECORD +49 -0
- fp_core-0.1.0.dist-info/WHEEL +5 -0
- fp_core-0.1.0.dist-info/top_level.txt +1 -0
fp_core/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""fp-core - 五块卵石 Agent 引擎"""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__license__ = "MIT"
|
|
5
|
+
__author__ = "zpb"
|
|
6
|
+
|
|
7
|
+
from fp_core.core.agent import Agent, Message, Response
|
|
8
|
+
from fp_core.core.lifecycle import HookContext, LifecycleHook, LifecycleManager
|
|
9
|
+
from fp_core.plugins.base.plugin import Plugin, PluginConfig, PluginRegistry
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Agent",
|
|
13
|
+
"Message",
|
|
14
|
+
"Response",
|
|
15
|
+
"LifecycleManager",
|
|
16
|
+
"LifecycleHook",
|
|
17
|
+
"HookContext",
|
|
18
|
+
"Plugin",
|
|
19
|
+
"PluginConfig",
|
|
20
|
+
"PluginRegistry",
|
|
21
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
commands/__init__.py — 命令注册表与自动发现
|
|
3
|
+
|
|
4
|
+
自动扫描 commands/ 目录下所有 .py 文件(排除 __init__.py),
|
|
5
|
+
导入每个模块并检查 name/execute 接口,构建命令名→模块的映射(含别名)。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import importlib
|
|
10
|
+
import importlib.util
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from fp_core import display
|
|
14
|
+
|
|
15
|
+
# 缓存:命令名 → 模块对象
|
|
16
|
+
_commands: dict[str, "CommandModule"] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# 类型标注
|
|
20
|
+
class CommandModule:
|
|
21
|
+
name: str
|
|
22
|
+
aliases: list[str]
|
|
23
|
+
description: str
|
|
24
|
+
|
|
25
|
+
# execute 返回 (已处理, 输出文本);
|
|
26
|
+
# 兼容旧版:也可只返回 bool(自动转为 ("", False/True))
|
|
27
|
+
# 同步或异步均可,由 execute() 自动适配
|
|
28
|
+
async def execute(self, arg: str) -> tuple[bool, str]: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _discover_commands():
|
|
32
|
+
"""扫描并注册所有命令模块(内置 → 用户,同名覆盖)"""
|
|
33
|
+
global _commands
|
|
34
|
+
_commands = {}
|
|
35
|
+
|
|
36
|
+
builtin_dir = os.path.dirname(os.path.abspath(__file__))
|
|
37
|
+
_scan_dir(builtin_dir, "fp_core.commands")
|
|
38
|
+
|
|
39
|
+
# 用户命令目录
|
|
40
|
+
_xdg_data = os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
|
41
|
+
user_dir = os.path.join(_xdg_data, "fp", "commands")
|
|
42
|
+
_scan_dir(user_dir) # 直接 import 路径,通过 sys.path 解析
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _scan_dir(directory: str, package_prefix: str | None = None):
|
|
46
|
+
"""扫描单个目录下的命令文件"""
|
|
47
|
+
if not os.path.isdir(directory):
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
for fname in sorted(os.listdir(directory)):
|
|
51
|
+
if not fname.endswith(".py") or fname == "__init__.py":
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
mod_name = fname[:-3]
|
|
55
|
+
try:
|
|
56
|
+
if package_prefix:
|
|
57
|
+
mod = importlib.import_module(f"{package_prefix}.{mod_name}")
|
|
58
|
+
else:
|
|
59
|
+
spec = importlib.util.spec_from_file_location(mod_name, os.path.join(directory, fname))
|
|
60
|
+
if spec is None or spec.loader is None:
|
|
61
|
+
continue
|
|
62
|
+
mod = importlib.util.module_from_spec(spec)
|
|
63
|
+
spec.loader.exec_module(mod)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
display.warning(f"⚠️ 命令加载失败 [{mod_name}]: {e}")
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# 校验接口
|
|
69
|
+
if not hasattr(mod, "name") or not hasattr(mod, "execute"):
|
|
70
|
+
display.warning(f"⚠️ 命令模块 [{mod_name}] 缺少 name/execute,已跳过")
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
name = mod.name
|
|
74
|
+
if name in _commands:
|
|
75
|
+
display.warning(f"⚠️ 命令 [{name}] 重复定义,已覆盖")
|
|
76
|
+
_commands[name] = mod
|
|
77
|
+
|
|
78
|
+
# 注册别名
|
|
79
|
+
for alias in getattr(mod, "aliases", []):
|
|
80
|
+
if alias in _commands:
|
|
81
|
+
display.warning(f"⚠️ 别名 [{alias}] 与已有命令/别名冲突,已跳过")
|
|
82
|
+
continue
|
|
83
|
+
_commands[alias] = mod
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_discover_commands()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_command(name: str) -> CommandModule | None:
|
|
90
|
+
"""根据命令名(含斜杠)或别名查找命令模块"""
|
|
91
|
+
return _commands.get(name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_all_commands() -> dict[str, str]:
|
|
95
|
+
"""返回 {命令名: 描述} 字典(只返回主名称,不含别名)"""
|
|
96
|
+
result: dict[str, str] = {}
|
|
97
|
+
seen: set[int] = set()
|
|
98
|
+
for name, mod in _commands.items():
|
|
99
|
+
mod_id = id(mod)
|
|
100
|
+
if mod_id in seen:
|
|
101
|
+
continue
|
|
102
|
+
if hasattr(mod, "name") and mod.name == name:
|
|
103
|
+
result[name] = getattr(mod, "description", "")
|
|
104
|
+
seen.add(mod_id)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def execute(agent, cmd_name: str, arg: str) -> tuple[bool, str]:
|
|
109
|
+
"""执行命令,返回 (是否已处理, 输出文本)。
|
|
110
|
+
|
|
111
|
+
兼容旧版只返回 bool 的命令(自动补为 ("", False/True))。
|
|
112
|
+
新版命令可返回 tuple[bool, str] 或 tuple[bool, str, str]。
|
|
113
|
+
"""
|
|
114
|
+
mod = get_command(cmd_name)
|
|
115
|
+
if mod is None:
|
|
116
|
+
return (False, "")
|
|
117
|
+
|
|
118
|
+
# 执行命令(自动适配同步/异步)
|
|
119
|
+
if asyncio.iscoroutinefunction(mod.execute):
|
|
120
|
+
result = await mod.execute(agent, arg)
|
|
121
|
+
else:
|
|
122
|
+
result = mod.execute(agent, arg)
|
|
123
|
+
|
|
124
|
+
# 兼容旧版:只返回 bool
|
|
125
|
+
if isinstance(result, bool):
|
|
126
|
+
return (result, "")
|
|
127
|
+
|
|
128
|
+
# 新版:返回 (handled, output)
|
|
129
|
+
if isinstance(result, tuple):
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
return (True, str(result))
|
fp_core/commands/back.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""back 命令 — 回退到对话的某个历史时刻
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
/back list 查看历史消息列表(仅非 system 消息,按 1-based 编号)
|
|
5
|
+
/back <index> 回退到指定位置(删除后续消息)
|
|
6
|
+
/back <index> 2 同上(删除后续消息)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
name = "back"
|
|
10
|
+
aliases = []
|
|
11
|
+
description = "回退到对话的某个历史时刻。用法: /back list 查看列表, /back <N> 直接回退"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def execute(agent, arg: str) -> tuple[bool, str]:
|
|
15
|
+
parts = arg.strip().split()
|
|
16
|
+
|
|
17
|
+
if not parts:
|
|
18
|
+
msg = "❌ 用法: /back list (查看列表) 或 /back <N> (直接回退)"
|
|
19
|
+
agent.io.error(msg)
|
|
20
|
+
return (True, msg)
|
|
21
|
+
|
|
22
|
+
cmd = parts[0]
|
|
23
|
+
|
|
24
|
+
# ── /back list ─────────────────────────────────────────────
|
|
25
|
+
if cmd == "list":
|
|
26
|
+
history_msgs = agent.get_history_for_display()
|
|
27
|
+
if not history_msgs:
|
|
28
|
+
msg = "没有历史记录"
|
|
29
|
+
agent.io.info(msg)
|
|
30
|
+
return (True, msg)
|
|
31
|
+
|
|
32
|
+
roles_zh = {"user": "👤 用户", "assistant": "🤖 AI", "tool": "🔧 工具"}
|
|
33
|
+
lines = [f"📜 对话历史(共 {len(history_msgs)} 条消息,使用 /back <N> 回退):"]
|
|
34
|
+
for i, msg in enumerate(history_msgs):
|
|
35
|
+
role = roles_zh.get(msg["role"], msg["role"])
|
|
36
|
+
content = msg.get("content", "")
|
|
37
|
+
if msg["role"] == "tool":
|
|
38
|
+
content = content[:60] + "..." if len(content) > 60 else content
|
|
39
|
+
else:
|
|
40
|
+
content = content[:120] + "..." if len(content) > 120 else content
|
|
41
|
+
content = content.replace("\n", " ")
|
|
42
|
+
lines.append(f" [{i + 1:3d}] {role}: {content}")
|
|
43
|
+
|
|
44
|
+
agent.io.info(lines[0])
|
|
45
|
+
for line in lines[1:]:
|
|
46
|
+
agent.io.item(line)
|
|
47
|
+
|
|
48
|
+
return (True, "\n".join(lines))
|
|
49
|
+
|
|
50
|
+
# ── /back <index> [2] ──────────────────────────────────────
|
|
51
|
+
try:
|
|
52
|
+
index = int(cmd)
|
|
53
|
+
except ValueError:
|
|
54
|
+
msg = f"❌ 无效参数:'{cmd}' — 请用 /back list 查看列表,/back <N> 回退"
|
|
55
|
+
agent.io.error(msg)
|
|
56
|
+
return (True, msg)
|
|
57
|
+
|
|
58
|
+
mode = None
|
|
59
|
+
if len(parts) >= 2:
|
|
60
|
+
try:
|
|
61
|
+
mode = int(parts[1])
|
|
62
|
+
if mode != 2:
|
|
63
|
+
msg = "❌ mode=1(保留后续消息)暂不支持,请用 mode=2 或 /fork"
|
|
64
|
+
agent.io.error(msg)
|
|
65
|
+
return (True, msg)
|
|
66
|
+
except ValueError:
|
|
67
|
+
msg = f"❌ 无效参数:'{parts[1]}' 不是数字"
|
|
68
|
+
agent.io.error(msg)
|
|
69
|
+
return (True, msg)
|
|
70
|
+
|
|
71
|
+
result = await agent.back(target_idx=index, mode=mode)
|
|
72
|
+
agent.io.info(result)
|
|
73
|
+
return (True, result)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""clear 命令 — 清空当前会话"""
|
|
2
|
+
|
|
3
|
+
from fp_core import display
|
|
4
|
+
|
|
5
|
+
name = "clear"
|
|
6
|
+
aliases = []
|
|
7
|
+
description = "清空当前会话"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(agent, arg: str) -> tuple[bool, str]:
|
|
11
|
+
agent.clear_session()
|
|
12
|
+
msg = "🧹 当前会话已清空"
|
|
13
|
+
display.info(msg)
|
|
14
|
+
return (True, msg)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""exit! 命令 — 核弹级退出:删除当前会话、不留痕迹"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from fp_core import display
|
|
6
|
+
|
|
7
|
+
name = "exit!"
|
|
8
|
+
aliases = []
|
|
9
|
+
description = "核弹级退出:删除当前会话、不留痕迹"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute(agent, arg: str) -> bool:
|
|
13
|
+
sid = agent.session.session_id
|
|
14
|
+
path = agent.session.get_session_path()
|
|
15
|
+
|
|
16
|
+
# 标记核弹退出 — shutdown 时会删除会话文件
|
|
17
|
+
agent.set_nuclear_exit()
|
|
18
|
+
|
|
19
|
+
# 提前删除文件(shutdown 也会删,双重保险)
|
|
20
|
+
if os.path.exists(path):
|
|
21
|
+
try:
|
|
22
|
+
os.remove(path)
|
|
23
|
+
display.info(f"💥 会话 {sid} 已删除,不留痕迹")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
display.warning(f"⚠️ 删除失败: {e}")
|
|
26
|
+
|
|
27
|
+
raise SystemExit()
|
fp_core/commands/fork.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""fork 命令 — 基于当前上下文新建会话"""
|
|
2
|
+
|
|
3
|
+
from fp_core import display
|
|
4
|
+
|
|
5
|
+
name = "fork"
|
|
6
|
+
aliases = []
|
|
7
|
+
description = "基于当前上下文新建会话"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(agent, arg: str) -> tuple[bool, str]:
|
|
11
|
+
result = agent.fork()
|
|
12
|
+
if result:
|
|
13
|
+
display.info(result)
|
|
14
|
+
return (True, result)
|
|
15
|
+
display.info("当前会话没有消息,无法 fork")
|
|
16
|
+
return (True, "")
|
fp_core/commands/help.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""help 命令 — 显示帮助信息"""
|
|
2
|
+
|
|
3
|
+
from fp_core import display
|
|
4
|
+
|
|
5
|
+
name = "help"
|
|
6
|
+
aliases = ["?"]
|
|
7
|
+
description = "显示此帮助"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(agent, arg: str) -> tuple[bool, str]:
|
|
11
|
+
from fp_core.commands import get_all_commands
|
|
12
|
+
|
|
13
|
+
cmds = get_all_commands()
|
|
14
|
+
lines = ["可用命令:"]
|
|
15
|
+
for c, d in sorted(cmds.items()):
|
|
16
|
+
lines.append(f" /{c:11s} {d}")
|
|
17
|
+
output = "\n".join(lines)
|
|
18
|
+
|
|
19
|
+
# CLI 模式:输出到终端(保持着色)
|
|
20
|
+
display.info("可用命令:")
|
|
21
|
+
for c, d in sorted(cmds.items()):
|
|
22
|
+
display.item(f" /{c:11s} {d}")
|
|
23
|
+
|
|
24
|
+
# WebUI 模式:通过返回值传递输出
|
|
25
|
+
return (True, output)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""history 命令 — 查看当前对话历史"""
|
|
2
|
+
|
|
3
|
+
from fp_core import display
|
|
4
|
+
|
|
5
|
+
name = "history"
|
|
6
|
+
aliases = []
|
|
7
|
+
description = "查看当前对话历史"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(agent, arg: str) -> tuple[bool, str]:
|
|
11
|
+
history_msgs = agent.history()
|
|
12
|
+
|
|
13
|
+
if not history_msgs:
|
|
14
|
+
display.info("暂无对话历史")
|
|
15
|
+
return (True, "")
|
|
16
|
+
|
|
17
|
+
roles_zh = {"user": "👤 用户", "assistant": "🤖 AI", "tool": "🔧 工具"}
|
|
18
|
+
display.info(f"\n📜 对话历史(共 {len(history_msgs)} 条):")
|
|
19
|
+
print()
|
|
20
|
+
|
|
21
|
+
for i, msg in enumerate(history_msgs):
|
|
22
|
+
role = roles_zh.get(msg["role"], msg["role"])
|
|
23
|
+
content = msg.get("content", "")
|
|
24
|
+
if msg["role"] == "tool":
|
|
25
|
+
content = content[:80] + "..." if len(content) > 80 else content
|
|
26
|
+
else:
|
|
27
|
+
content = content[:120] + "..." if len(content) > 120 else content
|
|
28
|
+
content = content.replace("\n", " ")
|
|
29
|
+
display.item(f" [{i + 1:3d}] {role}: {content}")
|
|
30
|
+
|
|
31
|
+
print()
|
|
32
|
+
return (True, "📜 历史已显示(终端)")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""memory 命令 — 查看持久化记忆"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from fp_core import config, display
|
|
6
|
+
|
|
7
|
+
name = "memory"
|
|
8
|
+
aliases = []
|
|
9
|
+
description = "查看持久化记忆"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute(agent, arg: str) -> tuple[bool, str]:
|
|
13
|
+
lines = ["📋 记忆列表:", ""]
|
|
14
|
+
memory_dir = config.MEMORY_DIR
|
|
15
|
+
if os.path.isdir(memory_dir):
|
|
16
|
+
for f in sorted(os.listdir(memory_dir)):
|
|
17
|
+
if f.endswith(".md"):
|
|
18
|
+
name = f[:-3]
|
|
19
|
+
lines.append(f" • {name}")
|
|
20
|
+
if len(lines) == 2:
|
|
21
|
+
lines.append(" (暂无记忆)")
|
|
22
|
+
output = "\n".join(lines)
|
|
23
|
+
hint = "💡 使用 memory_read / memory_save 工具管理记忆"
|
|
24
|
+
|
|
25
|
+
# CLI 模式:保持着色
|
|
26
|
+
display.info(output)
|
|
27
|
+
display.hint(hint)
|
|
28
|
+
|
|
29
|
+
# WebUI 模式:通过返回值传递
|
|
30
|
+
return (True, output + "\n" + hint)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""model 命令 — 显示当前模型配置"""
|
|
2
|
+
|
|
3
|
+
from fp_core import config, display
|
|
4
|
+
|
|
5
|
+
name = "model"
|
|
6
|
+
aliases = []
|
|
7
|
+
description = "显示当前模型配置"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(agent, arg: str) -> tuple[bool, str]:
|
|
11
|
+
lines = [
|
|
12
|
+
f"模型: {agent.model}",
|
|
13
|
+
f"温度: {config.LLM_TEMPERATURE}",
|
|
14
|
+
f"最大 Token: {config.LLM_MAX_TOKENS}",
|
|
15
|
+
f"会话目录: {config.SESSIONS_DIR}",
|
|
16
|
+
]
|
|
17
|
+
output = "\n".join(lines)
|
|
18
|
+
|
|
19
|
+
# CLI 模式
|
|
20
|
+
for line in lines:
|
|
21
|
+
display.info(line)
|
|
22
|
+
|
|
23
|
+
# WebUI 模式
|
|
24
|
+
return (True, output)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""resume 命令 — 切换/删除历史会话(非交互式)
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
/resume list 列出所有会话
|
|
5
|
+
/resume latest 切换到最新会话
|
|
6
|
+
/resume <sid> 切换到指定会话
|
|
7
|
+
/resume delete list 列出可删除的会话
|
|
8
|
+
/resume delete <sid> 删除指定会话
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
name = "resume"
|
|
12
|
+
aliases = []
|
|
13
|
+
description = "切换/删除历史会话。用法: /resume list, /resume latest, /resume <sid>, /resume delete <sid>"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _display_width(s: str) -> int:
|
|
17
|
+
"""计算字符串的显示宽度(中文=2,英文/数字/符号=1)"""
|
|
18
|
+
width = 0
|
|
19
|
+
for c in s:
|
|
20
|
+
if "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or c in "()":
|
|
21
|
+
width += 2
|
|
22
|
+
else:
|
|
23
|
+
width += 1
|
|
24
|
+
return width
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _pad_to_width(s: str, width: int) -> str:
|
|
28
|
+
"""用空格填充到指定显示宽度"""
|
|
29
|
+
return s + " " * max(0, width - _display_width(s))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def execute(agent, arg: str) -> tuple[bool, str]:
|
|
33
|
+
arg = arg.strip()
|
|
34
|
+
|
|
35
|
+
# ── /resume delete list ────────────────────────────────────
|
|
36
|
+
if arg == "delete list":
|
|
37
|
+
sessions = agent.session.list_sessions()
|
|
38
|
+
if not sessions:
|
|
39
|
+
msg = "暂无历史会话"
|
|
40
|
+
agent.io.info(msg)
|
|
41
|
+
return (True, msg)
|
|
42
|
+
|
|
43
|
+
current_sid = agent.session.session_id
|
|
44
|
+
sorted_items = sorted(
|
|
45
|
+
sessions.items(),
|
|
46
|
+
key=lambda x: x[1].get("updated", ""),
|
|
47
|
+
reverse=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
deletable = [(sid, meta) for sid, meta in sorted_items if sid != current_sid]
|
|
51
|
+
if not deletable:
|
|
52
|
+
msg = "没有可删除的会话(当前会话不可删除)"
|
|
53
|
+
agent.io.info(msg)
|
|
54
|
+
return (True, msg)
|
|
55
|
+
|
|
56
|
+
summary_width = 44
|
|
57
|
+
lines = ["🗑️ 可删除的会话(使用 /resume delete <sid> 删除):"]
|
|
58
|
+
for i, (sid, meta) in enumerate(deletable, 1):
|
|
59
|
+
raw_summary = meta.get("summary", "") or ""
|
|
60
|
+
msg_count = meta.get("message_count", 0)
|
|
61
|
+
created = meta.get("created", "?")[:16]
|
|
62
|
+
if not raw_summary:
|
|
63
|
+
display_summary = "(无摘要)"
|
|
64
|
+
else:
|
|
65
|
+
display_summary = ""
|
|
66
|
+
for ch in raw_summary:
|
|
67
|
+
candidate = display_summary + ch
|
|
68
|
+
if _display_width(candidate) > summary_width - 1:
|
|
69
|
+
display_summary += "…"
|
|
70
|
+
break
|
|
71
|
+
display_summary = candidate
|
|
72
|
+
line = f" [{i}] {_pad_to_width(display_summary, summary_width)} ({msg_count:3d}条, {created}) [{sid}]"
|
|
73
|
+
lines.append(line)
|
|
74
|
+
|
|
75
|
+
agent.io.info(lines[0])
|
|
76
|
+
for line in lines[1:]:
|
|
77
|
+
agent.io.item(line)
|
|
78
|
+
|
|
79
|
+
return (True, "\n".join(lines))
|
|
80
|
+
|
|
81
|
+
# ── /resume delete <sid> ───────────────────────────────────
|
|
82
|
+
if arg.startswith("delete "):
|
|
83
|
+
sid = arg[7:].strip()
|
|
84
|
+
if sid == agent.session.session_id:
|
|
85
|
+
msg = "❌ 不能删除当前正在使用的会话"
|
|
86
|
+
agent.io.error(msg)
|
|
87
|
+
return (True, msg)
|
|
88
|
+
if agent.delete_session(sid):
|
|
89
|
+
msg = f"🗑️ 已删除会话: {sid}"
|
|
90
|
+
agent.io.info(msg)
|
|
91
|
+
return (True, msg)
|
|
92
|
+
else:
|
|
93
|
+
msg = f"❌ 会话 {sid} 不存在或删除失败"
|
|
94
|
+
agent.io.error(msg)
|
|
95
|
+
return (True, msg)
|
|
96
|
+
|
|
97
|
+
# ── /resume delete (无参数) ─────────────────────────────────
|
|
98
|
+
if arg == "delete":
|
|
99
|
+
msg = "❌ 用法: /resume delete list (查看可删除会话) 或 /resume delete <sid> (直接删除)"
|
|
100
|
+
agent.io.error(msg)
|
|
101
|
+
return (True, msg)
|
|
102
|
+
|
|
103
|
+
# ── /resume list ───────────────────────────────────────────
|
|
104
|
+
if arg == "list":
|
|
105
|
+
sessions = agent.session.list_sessions()
|
|
106
|
+
if not sessions:
|
|
107
|
+
msg = "暂无历史会话"
|
|
108
|
+
agent.io.info(msg)
|
|
109
|
+
return (True, msg)
|
|
110
|
+
|
|
111
|
+
current_sid = agent.session.session_id
|
|
112
|
+
sorted_items = sorted(
|
|
113
|
+
sessions.items(),
|
|
114
|
+
key=lambda x: x[1].get("updated", ""),
|
|
115
|
+
reverse=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
summary_width = 44
|
|
119
|
+
lines = ["📂 会话列表(使用 /resume <sid> 切换,/resume latest 切换到最新):"]
|
|
120
|
+
for i, (sid, meta) in enumerate(sorted_items, 1):
|
|
121
|
+
raw_summary = meta.get("summary", "") or ""
|
|
122
|
+
msg_count = meta.get("message_count", 0)
|
|
123
|
+
marker = " ⬅" if sid == current_sid else ""
|
|
124
|
+
|
|
125
|
+
if not raw_summary:
|
|
126
|
+
display_summary = "(无摘要)"
|
|
127
|
+
else:
|
|
128
|
+
display_summary = ""
|
|
129
|
+
for ch in raw_summary:
|
|
130
|
+
candidate = display_summary + ch
|
|
131
|
+
if _display_width(candidate) > summary_width - 1:
|
|
132
|
+
display_summary += "…"
|
|
133
|
+
break
|
|
134
|
+
display_summary = candidate
|
|
135
|
+
|
|
136
|
+
line = f" [{i}] {_pad_to_width(display_summary, summary_width)} ({msg_count:3d}条, {sid}){marker}"
|
|
137
|
+
lines.append(line)
|
|
138
|
+
|
|
139
|
+
agent.io.info(lines[0])
|
|
140
|
+
for line in lines[1:]:
|
|
141
|
+
agent.io.item(line)
|
|
142
|
+
|
|
143
|
+
return (True, "\n".join(lines))
|
|
144
|
+
|
|
145
|
+
# ── /resume latest ─────────────────────────────────────────
|
|
146
|
+
if arg == "latest":
|
|
147
|
+
agent.resume_latest()
|
|
148
|
+
msg = f"📂 已切换到最新会话: {agent.session.session_id}"
|
|
149
|
+
agent.io.info(msg)
|
|
150
|
+
return (True, msg)
|
|
151
|
+
|
|
152
|
+
# ── /resume <无参数> ────────────────────────────────────────
|
|
153
|
+
if not arg:
|
|
154
|
+
msg = "❌ 用法: /resume list (查看列表) | /resume latest | /resume <sid> (直接切换)"
|
|
155
|
+
agent.io.error(msg)
|
|
156
|
+
return (True, msg)
|
|
157
|
+
|
|
158
|
+
# ── /resume <sid> ──────────────────────────────────────────
|
|
159
|
+
if agent.switch_session(arg):
|
|
160
|
+
msg = f"📂 已切换到会话: {arg}"
|
|
161
|
+
agent.io.info(msg)
|
|
162
|
+
return (True, msg)
|
|
163
|
+
else:
|
|
164
|
+
msg = f"❌ 会话 {arg} 不存在。使用 /resume list 查看可用会话"
|
|
165
|
+
agent.io.error(msg)
|
|
166
|
+
return (True, msg)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""session 命令 — 显示当前会话信息"""
|
|
2
|
+
|
|
3
|
+
from fp_core import display
|
|
4
|
+
|
|
5
|
+
name = "session"
|
|
6
|
+
aliases = []
|
|
7
|
+
description = "显示当前会话信息"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(agent, arg: str) -> tuple[bool, str]:
|
|
11
|
+
msg = f"📂 当前会话: {agent.session.session_id}"
|
|
12
|
+
display.info(msg)
|
|
13
|
+
return (True, msg)
|