diff --git a/core/urls.py b/core/urls.py index 98b7c95..0f8e259 100644 --- a/core/urls.py +++ b/core/urls.py @@ -7,7 +7,6 @@ from django.views.generic import TemplateView from django.http import FileResponse, Http404 from links.views import LinkDetailView, LinkUpdateView, CustomLinkView from links.file_views import PublicFileView, import_image_view -from links.ask_views import ask_page as ask_page_view from django.urls import path, include, re_path from django.conf.urls.i18n import i18n_patterns import os @@ -61,10 +60,6 @@ urlpatterns = [ # JBOT AI Robot Face SPA path('jbot/', include('jbot.urls')), - # Ask — 手机一键提问(必须在 links.urls 的 alias catch-all 之前) - path('ask/', ask_page_view, name='ask-page'), - path('ask//', ask_page_view, name='ask-page-q'), - # Import external image by URL — /import/images/ (also plural alias) path('import/images/', import_image_view, name='import-image'), path('imports/images/', import_image_view, name='imports-image'), diff --git a/links/api_urls.py b/links/api_urls.py index 11ef576..59faa9c 100644 --- a/links/api_urls.py +++ b/links/api_urls.py @@ -5,7 +5,6 @@ from . import post_views from . import api_views from . import file_views from . import bookmark_views -from . import ask_views # Create a router and register our viewsets with it router = DefaultRouter(trailing_slash=False) @@ -19,7 +18,6 @@ router.register('bookmarks', bookmark_views.BookmarkViewSet, basename='api-bookm # The API URLs are determined automatically by the router urlpatterns = [ path('', include(router.urls)), - path('ask/', ask_views.ask_api, name='api-ask'), path('images/', include('links.image_urls')), ] diff --git a/links/ask_views.py b/links/ask_views.py deleted file mode 100644 index dd8ca88..0000000 --- a/links/ask_views.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Ask view — 手机一键提问入口(方案 B:投递到 Discord)。 - -用户打开 http://go/ask/我的问题 即可把问题直接发到 Discord #general 并 @小黑, -小黑在 Discord 里原生回复(含工具调用过程、长文排版),无需打开 Discord 操作。 - -调用链:手机浏览器 → links Django → Discord Webhook(#general)→ 小黑回复在 Discord。 -Webhook URL 通过环境变量 DISCORD_WEBHOOK_URL 配置(K8s deployment 注入)。 - -页面同时提供「打开 Discord」按钮(https universal link 直达 #general 频道)。 - -发送逻辑复用 links/actions.py(动作链接共用同一套 webhook 发送)。 -""" -import logging -import os - -from django.http import JsonResponse -from django.shortcuts import render, redirect -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_POST - -from .actions import MAX_MSG_LEN, execute_action, make_dedup_key, send_discord_message - -logger = logging.getLogger(__name__) - -DISCORD_BOT_ID = os.environ.get('DISCORD_BOT_ID', '1467845106831855796') -# 用 https universal link(手机上唤起 APP,桌面上打开网页版), -# 比 discord:// custom scheme 兼容性更好(后者在桌面浏览器不可用)。 -DISCORD_DEEPLINK = os.environ.get( - 'DISCORD_DEEPLINK', - 'https://discord.com/channels/1467846046590959798/1467846047089954952', -) - - -def ask_page(request, question=None): - """提问入口。 - - - 带问题(/ask/问题 或 ?q=问题):直接发消息到 Discord 并 302 跳转, - 不显示页面;10 分钟内同一问题不重复发送。 - - 不带问题(/ask/):渲染移动端输入页(JS 调 /api/ask/ 发送)。 - """ - if question is None: - question = request.GET.get('q', '') - question = (question or '').strip() - - if question: - dedup_key = make_dedup_key('ask', question) - try: - result = execute_action( - {'action_type': 'discord_send', 'message_template': f'<@{DISCORD_BOT_ID}> {{query}}'}, - question, - dedup_key=dedup_key, - ) - except Exception as e: - logger.warning('[ASK] failed: %s', e) - result = {'success': False, 'message': str(e), 'discord_deeplink': '', 'deduped': False} - if result.get('success') and result.get('discord_deeplink'): - return redirect(result['discord_deeplink']) - return render(request, 'links/action_result.html', { - 'result': result, - 'query': question, - 'link': None, - }) - - return render(request, 'links/ask.html', { - 'initial_question': '', - 'discord_deeplink': DISCORD_DEEPLINK, - }) - - -@csrf_exempt -@require_POST -def ask_api(request): - """POST /api/ask/ body: {"q": "问题"} → {"text": "提示文案"}""" - import json as _json - - try: - data = _json.loads(request.body or b'{}') - except Exception: - return JsonResponse({'error': '无效的请求体'}, status=400) - - question = (data.get('q') or '').strip() - if not question: - return JsonResponse({'error': '问题不能为空'}, status=400) - if len(question) > MAX_MSG_LEN: - return JsonResponse({'error': '问题太长了(最多 1900 字)'}, status=400) - - try: - dedup_key = make_dedup_key('ask', question) - result = execute_action( - {'action_type': 'discord_send', 'message_template': f'<@{DISCORD_BOT_ID}> {{query}}'}, - question, - dedup_key=dedup_key, - ) - except Exception as e: - logger.warning('[ASK] post failed: %s', e) - return JsonResponse({'error': str(e)}, status=502) - - if result.get('deduped'): - return JsonResponse({'text': '10 分钟内已发送过相同问题,不重复发送'}) - return JsonResponse({'text': '已投递到 Discord,小黑回复会出现在 #general'}) diff --git a/links/migrations/0053_ask_as_action_link.py b/links/migrations/0053_ask_as_action_link.py new file mode 100644 index 0000000..4e7e156 --- /dev/null +++ b/links/migrations/0053_ask_as_action_link.py @@ -0,0 +1,44 @@ +""" +Ask 已从固定路由改为普通 ACTION Link(alias='ask')。 + +访问 go/ask/问题 → alias catch-all → redirect_to_original → ACTION 分支 +→ execute_action(discord_send) → 302 跳转 Discord。与用户手动创建的 +动作链接行为完全一致,可在 Links 界面编辑/删除。 + +message_template 使用 <@{bot_id}> 让 Discord 消息 @小黑,bot_id 与 +原 ask_views 默认值一致(1467845106831855796)。 +""" +from django.db import migrations + +DISCORD_BOT_ID = '1467845106831855796' + + +def create_ask_action_link(apps, schema_editor): + Link = apps.get_model('links', 'Link') + Link.objects.get_or_create( + alias='ask', + defaults={ + 'link_type': 'ACTION', + 'action_config': { + 'action_type': 'discord_send', + 'message_template': f'<@{DISCORD_BOT_ID}> {{query}}', + }, + 'description': '一键提问小黑:go/ask/问题(发到 Discord #general)', + }, + ) + + +def remove_ask_action_link(apps, schema_editor): + Link = apps.get_model('links', 'Link') + Link.objects.filter(alias='ask', link_type='ACTION').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('links', '0052_link_action_config_alter_link_link_type'), + ] + + operations = [ + migrations.RunPython(create_ask_action_link, remove_ask_action_link), + ] diff --git a/templates/links/ask.html b/templates/links/ask.html deleted file mode 100644 index 134a8b0..0000000 --- a/templates/links/ask.html +++ /dev/null @@ -1,386 +0,0 @@ -{% load static %} - - - - - - - 问小黑 - - - - -
-
-
-
问小黑
-
Hermes Agent · 在线
-
-
- -
-
-
🖤
- 直接输入问题,小黑马上回答。
- 比如「今天墨尔本天气怎么样」 -
也可以直接在链接后加问题:
go/ask/你的问题
-
-
- -
- - -
- - - - diff --git a/tests/test_actions.py b/tests/test_actions.py index 3361ce4..9542bf2 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -174,10 +174,29 @@ class TestRedirectAction: assert resp['Location'] == 'https://example.com/path' -# ── /ask/ 路由(带问题 = 发送+跳转;不带 = 输入页)───────────────────────── +# ── ask 作为普通 ACTION Link(alias='ask',非固定路由)──────────────────────── -class TestAskRoute: - def test_ask_with_question_redirects(self, monkeypatch): +class TestAskActionLink: + """Ask 不再有固定路由:它是一条 link_type=ACTION 的普通 Link(alias='ask'), + 由 data migration 0053 创建。访问 go/ask/问题 走 alias catch-all → + redirect_to_original → ACTION 分支,与其他动作链接行为一致。""" + + def _create_ask_link(self, db): + # data migration 0053 已创建 alias='ask';测试用 get_or_create 复用 + link, _ = Link.objects.get_or_create( + alias='ask', + defaults={ + 'link_type': Link.LinkType.ACTION, + 'action_config': { + 'action_type': 'discord_send', + 'message_template': '<@1467845106831855796> {query}', + }, + }, + ) + return link + + def test_ask_with_question_redirects(self, db, monkeypatch): + self._create_ask_link(db) monkeypatch.setenv('DISCORD_WEBHOOK_URL', 'https://example.com/hook') client = Client() with patch('links.actions.requests.post') as mock_post: @@ -186,8 +205,10 @@ class TestAskRoute: assert resp['Location'].startswith('https://discord.com/channels/') body = mock_post.call_args.kwargs['json']['content'] assert '今天有什么新闻' in body + assert '<@1467845106831855796>' in body - def test_ask_with_question_dedup(self, monkeypatch): + def test_ask_with_question_dedup(self, db, monkeypatch): + self._create_ask_link(db) monkeypatch.setenv('DISCORD_WEBHOOK_URL', 'https://example.com/hook') client = Client() with patch('links.actions.requests.post') as mock_post: @@ -195,8 +216,11 @@ class TestAskRoute: client.get('/ask/相同问题/') assert mock_post.call_count == 1 - def test_ask_empty_renders_input_page(self): + def test_ask_empty_query_renders_error_not_input_page(self, db): + self._create_ask_link(db) client = Client() resp = client.get('/ask/') + # 无参数时 ACTION 链接渲染错误提示页(不再有 ask.html 输入页) assert resp.status_code == 200 - assert '问小黑' in resp.content.decode() + assert '缺少参数' in resp.content.decode() + assert 'ask.html' not in resp.content.decode()