Skip to content

Commit 9a11f4e

Browse files
committed
Release v0.8.4: AI配置中心与图库专注模式优化
1 parent 6155bbf commit 9a11f4e

11 files changed

Lines changed: 1259 additions & 40 deletions

File tree

docs/release_notes.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Release Notes
2+
3+
## v0.8.4 (2026-02-20)
4+
- 新增 AI Prompt 配置能力:支持在设置页自定义 system/user prompt、temperature、tags_limit,并支持重置默认值。
5+
- 新增 LLM 运行时配置:支持在设置页配置 `base_url``model_name`,并展示 API Key 是否已配置。
6+
- 图片详情页支持“仅本次覆盖生成”高级参数;元数据返回 `prompt_source``llm_source` 便于追踪来源。
7+
- Gallery 弹层新增单击图片专注模式:仅隐藏左下角 AI 标题/标签覆层,保留右下角工具按钮可操作。
8+
- 新增 `tests/test_prompt_config.py`,覆盖 prompt/llm 配置 API 与元数据来源优先级。

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tiklocal",
3-
"version": "0.1.6",
3+
"version": "0.8.4",
44
"description": "A local media server that combines the features of TikTok and Pinterest",
55
"scripts": {
66
"build-css": "tailwindcss -i ./tiklocal/static/input.css -o ./tiklocal/static/output.css --watch",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "TikLocal"
3-
version = "0.8.3"
3+
version = "0.8.4"
44
description = "A local media server that combines the features of TikTok and Pinterest"
55
authors = ["ChanMo <chan.mo@outlook.com>"]
66
readme = "README.md"

tests/test_prompt_config.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import pytest
2+
3+
from tiklocal.app import create_app
4+
from tiklocal.services.metadata import PromptConfigStore
5+
6+
7+
def _build_prompt_payload(enabled: bool = True, tags_limit: int = 5, temperature: float = 0.6):
8+
return {
9+
"enabled": enabled,
10+
"system_prompt": "你是一个测试助手,只能输出 JSON。",
11+
"user_prompt": "请给出标题和 {tags_limit} 个标签。",
12+
"temperature": temperature,
13+
"tags_limit": tags_limit,
14+
}
15+
16+
17+
@pytest.fixture
18+
def client(tmp_path, monkeypatch):
19+
media_root = tmp_path / "media"
20+
media_root.mkdir(parents=True, exist_ok=True)
21+
(media_root / "photo.jpg").write_bytes(b"fake-image")
22+
23+
data_root = tmp_path / "tiklocal-data"
24+
monkeypatch.setenv("MEDIA_ROOT", str(media_root))
25+
monkeypatch.setenv("TIKLOCAL_INSTANCE", str(data_root))
26+
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
27+
monkeypatch.setenv("TIKLOCAL_LLM_MODEL", "test-model")
28+
29+
calls = []
30+
31+
def fake_generate(self, image_path, tags_limit=5, prompt_config=None): # noqa: ARG001
32+
calls.append({"tags_limit": tags_limit, "prompt_config": prompt_config or {}})
33+
return {
34+
"title": "测试标题",
35+
"tags": ["测试"],
36+
"model": self.model,
37+
"provider": "openai",
38+
"base_url": self.base_url or "",
39+
"prompt_version": 2,
40+
"prompt_hash": "deadbeef",
41+
}
42+
43+
monkeypatch.setattr("tiklocal.services.metadata.CaptionService.generate", fake_generate)
44+
45+
app = create_app({"TESTING": True, "MEDIA_ROOT": media_root})
46+
return app.test_client(), calls
47+
48+
49+
def test_prompt_config_store_roundtrip(tmp_path):
50+
store_path = tmp_path / "prompt_config.json"
51+
store = PromptConfigStore(store_path)
52+
53+
assert store.get() is None
54+
55+
saved = store.set(_build_prompt_payload())
56+
assert saved["enabled"] is True
57+
assert "updated_at" in saved
58+
59+
loaded = store.get()
60+
assert loaded is not None
61+
assert loaded["system_prompt"] == "你是一个测试助手,只能输出 JSON。"
62+
assert loaded["tags_limit"] == 5
63+
64+
store.reset()
65+
assert store.get() is None
66+
67+
68+
def test_prompt_config_api_crud(client):
69+
test_client, _ = client
70+
71+
res = test_client.get("/api/ai/prompt-config")
72+
data = res.get_json()
73+
assert res.status_code == 200
74+
assert data["success"] is True
75+
assert data["data"]["active_profile"] == "default"
76+
77+
save_payload = _build_prompt_payload(enabled=True, tags_limit=7, temperature=0.8)
78+
res = test_client.post("/api/ai/prompt-config", json=save_payload)
79+
data = res.get_json()
80+
assert res.status_code == 200
81+
assert data["success"] is True
82+
assert data["data"]["active_profile"] == "custom"
83+
assert data["data"]["custom"]["tags_limit"] == 7
84+
assert data["data"]["custom"]["enabled"] is True
85+
86+
res = test_client.post("/api/ai/prompt-config/reset")
87+
data = res.get_json()
88+
assert res.status_code == 200
89+
assert data["success"] is True
90+
assert data["data"]["active_profile"] == "default"
91+
assert data["data"]["custom"] is None
92+
93+
94+
def test_prompt_config_api_validation(client):
95+
test_client, _ = client
96+
payload = _build_prompt_payload(temperature=2.5)
97+
98+
res = test_client.post("/api/ai/prompt-config", json=payload)
99+
data = res.get_json()
100+
assert res.status_code == 400
101+
assert data["success"] is False
102+
assert "temperature" in data["error"]
103+
104+
105+
def test_metadata_prompt_source_priority(client):
106+
test_client, calls = client
107+
108+
custom_payload = _build_prompt_payload(enabled=True, tags_limit=7, temperature=0.7)
109+
res = test_client.post("/api/ai/prompt-config", json=custom_payload)
110+
assert res.status_code == 200
111+
112+
res = test_client.post(
113+
"/api/image/metadata",
114+
json={"uri": "photo.jpg", "force": True},
115+
)
116+
data = res.get_json()
117+
assert res.status_code == 200
118+
assert data["success"] is True
119+
assert data["data"]["prompt_source"] == "custom"
120+
assert calls[-1]["prompt_config"]["tags_limit"] == 7
121+
122+
override = {
123+
"system_prompt": "仅本次覆盖",
124+
"user_prompt": "请输出 1 到 {tags_limit} 个标签",
125+
"temperature": 0.3,
126+
"tags_limit": 3,
127+
}
128+
res = test_client.post(
129+
"/api/image/metadata",
130+
json={"uri": "photo.jpg", "force": True, "prompt_override": override},
131+
)
132+
data = res.get_json()
133+
assert res.status_code == 200
134+
assert data["success"] is True
135+
assert data["data"]["prompt_source"] == "override"
136+
assert calls[-1]["prompt_config"]["tags_limit"] == 3
137+
138+
139+
def test_llm_config_api_crud(client):
140+
test_client, _ = client
141+
142+
res = test_client.get("/api/ai/llm-config")
143+
data = res.get_json()
144+
assert res.status_code == 200
145+
assert data["success"] is True
146+
assert "effective" in data["data"]
147+
assert "has_api_key" in data["data"]
148+
149+
payload = {
150+
"model_name": "gpt-test-custom",
151+
"base_url": "https://example.com/v1",
152+
}
153+
res = test_client.post("/api/ai/llm-config", json=payload)
154+
data = res.get_json()
155+
assert res.status_code == 200
156+
assert data["success"] is True
157+
assert data["data"]["effective"]["model_name"] == "gpt-test-custom"
158+
assert data["data"]["effective"]["base_url"] == "https://example.com/v1"
159+
assert data["data"]["active_profile"] == "custom"
160+
161+
res = test_client.post("/api/ai/llm-config/reset")
162+
data = res.get_json()
163+
assert res.status_code == 200
164+
assert data["success"] is True
165+
assert data["data"]["active_profile"] == "default"
166+
167+
168+
def test_llm_config_api_validation(client):
169+
test_client, _ = client
170+
res = test_client.post(
171+
"/api/ai/llm-config",
172+
json={"model_name": "gpt-test", "base_url": "ftp://invalid-url"},
173+
)
174+
data = res.get_json()
175+
assert res.status_code == 400
176+
assert data["success"] is False
177+
assert "base_url" in data["error"]
178+
179+
180+
def test_metadata_uses_custom_llm_settings(client):
181+
test_client, _ = client
182+
183+
res = test_client.post(
184+
"/api/ai/llm-config",
185+
json={"model_name": "gpt-override", "base_url": "https://custom.example/v1"},
186+
)
187+
assert res.status_code == 200
188+
189+
res = test_client.post("/api/image/metadata", json={"uri": "photo.jpg", "force": True})
190+
data = res.get_json()
191+
assert res.status_code == 200
192+
assert data["success"] is True
193+
assert data["data"]["model"] == "gpt-override"
194+
assert data["data"]["base_url"] == "https://custom.example/v1"
195+
assert data["data"]["llm_source"] == "custom"

0 commit comments

Comments
 (0)