From 1c4948b9654a894902ea787ae28df33b05019a27 Mon Sep 17 00:00:00 2001 From: OpenClaw Sub-agent Date: Fri, 31 Jul 2026 22:21:54 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20/ask/=20quick-question=20page=20?= =?UTF-8?q?=E2=80=94=20mobile=20one-tap=20send=20to=20Discord=20via=20webh?= =?UTF-8?q?ook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/urls.py | 5 + k8s/manifest.template.yaml | 5 + links/api_urls.py | 2 + links/ask_views.py | 82 ++++++++ templates/links/ask.html | 386 +++++++++++++++++++++++++++++++++++++ 5 files changed, 480 insertions(+) create mode 100644 links/ask_views.py create mode 100644 templates/links/ask.html diff --git a/core/urls.py b/core/urls.py index 0f8e259..98b7c95 100644 --- a/core/urls.py +++ b/core/urls.py @@ -7,6 +7,7 @@ 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 @@ -60,6 +61,10 @@ 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/k8s/manifest.template.yaml b/k8s/manifest.template.yaml index c761c5f..462b74e 100644 --- a/k8s/manifest.template.yaml +++ b/k8s/manifest.template.yaml @@ -163,6 +163,11 @@ spec: key: key_id - name: FINNHUB_API_KEY value: "d7hbngpr01qhiu0b2pv0d7hbngpr01qhiu0b2pvg" + - name: DISCORD_WEBHOOK_URL + valueFrom: + secretKeyRef: + name: links-discord-webhook + key: webhook_url - name: REDIS_URL value: "redis://192.168.1.2:6379/0" - name: CRAWL4AI_API_URL diff --git a/links/api_urls.py b/links/api_urls.py index 59faa9c..11ef576 100644 --- a/links/api_urls.py +++ b/links/api_urls.py @@ -5,6 +5,7 @@ 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) @@ -18,6 +19,7 @@ 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 new file mode 100644 index 0000000..e684b06 --- /dev/null +++ b/links/ask_views.py @@ -0,0 +1,82 @@ +""" +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」按钮(discord:// deep link 直达 #general 频道)。 +""" +import logging +import os +import re + +import requests +from django.http import JsonResponse +from django.shortcuts import render +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST + +logger = logging.getLogger(__name__) + +DISCORD_BOT_ID = os.environ.get('DISCORD_BOT_ID', '1467845106831855796') +DISCORD_DEEPLINK = os.environ.get( + 'DISCORD_DEEPLINK', + 'discord://channels/1467846046590959798/1467846047089954952', +) + +# 用 webhook 发消息(限制长度,Discord 单条消息上限 2000 字符) +MAX_MSG_LEN = 1900 + + +def _post_to_discord(question: str) -> None: + """通过 Discord webhook 把问题发到 #general 并 @小黑。""" + webhook_url = os.environ.get('DISCORD_WEBHOOK_URL', '').strip() + if not webhook_url: + raise RuntimeError('服务端未配置 DISCORD_WEBHOOK_URL,请先配置') + + content = f'<@{DISCORD_BOT_ID}> {question}'[:MAX_MSG_LEN] + try: + resp = requests.post(webhook_url, json={'content': content}, timeout=15) + resp.raise_for_status() + except requests.exceptions.RequestException as e: + logger.warning('[ASK] discord webhook failed: %s', e) + raise RuntimeError('发送到 Discord 失败,稍后再试') + + +def ask_page(request, question=None): + """渲染提问页。question 可为 URL path 段或 ?q= 参数。""" + if question is None: + question = request.GET.get('q', '') + return render(request, 'links/ask.html', { + 'initial_question': 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: + _post_to_discord(question) + except Exception as e: + logger.warning('[ASK] post failed: %s', e) + return JsonResponse({'error': str(e)}, status=502) + + return JsonResponse({'text': '已投递到 Discord,小黑回复会出现在 #general'}) diff --git a/templates/links/ask.html b/templates/links/ask.html new file mode 100644 index 0000000..134a8b0 --- /dev/null +++ b/templates/links/ask.html @@ -0,0 +1,386 @@ +{% load static %} + + + + + + + 问小黑 + + + + +
+
+
+
问小黑
+
Hermes Agent · 在线
+
+
+ +
+
+
🖤
+ 直接输入问题,小黑马上回答。
+ 比如「今天墨尔本天气怎么样」 +
也可以直接在链接后加问题:
go/ask/你的问题
+
+
+ +
+ + +
+ + + +