做个ChatGPT的App玩玩儿

4,918 阅读6分钟

前言

前段时间比较闲,正好chatGpt风头很大,想着瞅瞅做个App玩玩儿,找遍全网都没看到类似Android的实现,没办法只能自己去看文档了,还能写篇文章啥的

直到我点开文档才知道。。。好家伙,这不就是接口拿数据?这做客户端的xdm可太懂了呀,这文章写出来不是侮辱人嘛

94614d406b99272dd3b699449d20759f.png

本来想就做个App玩玩儿算了,直到后来,竟然有好几个朋友都向我要!!??我才知道,好家伙你们不是不会啊,是真懒啊

006APoFYly1fzz03412jsg30dc09x3zf.jfif

思来想去还是把这个写出来吧,一来记录下自己、二来给xdm一点思路,写的不好xdm别见笑,话不多说,走起!

一、准备工作

首先还是得有ChatGpt的账号以及魔法能力(调用API),账号怎么注册各大博主写的太清楚了,我就不再赘述了,不清楚的可以搜一下,至于魔法能力。。。OK,准备就绪,咱们直接开始。

直接打开OpenAi的API官网:platform.openai.com/overview

登录之后,去生成一个KEY,保存一下,这个KEY一会儿要用

1685094741166.png

1685348293899.png

然后点击API,找到Chat

1685348663531.png

这里信息很齐全,做一个简单的请求我们只需要提供必选的信息即可,其他的可选参数大家感兴趣可以自己看看

简单归类一下这里的信息:

请求类型为POST

请求API为:api.openai.com/v1/chat/com…

model(模型)选择 :gpt-3.5-turbo(更多模型可以查看:platform.openai.com/docs/models

请求头需要添加两个字段,Content-Type(类型)和Authorization(KEY)

请求参数最少为model(模型)和messages , 这里注意下流式输出要添加stream = true参数(非必选)

响应格式为

1685349435889.png

ok,信息很全,拿出吃饭的家伙

二、完整代码

先撸个简单的ui

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragment.ChatFragment"
    android:background="@color/c_f7f8fa"
    android:id="@+id/frag_add">

    <!-- TODO: Update blank fragment layout -->
    <LinearLayout
        android:layout_height="match_parent"
        android:id="@+id/linear_add"
        android:layout_gravity="center_vertical"
         android:layout_width="match_parent"
        android:orientation="vertical">
    <ScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="6"
        >

        <LinearLayout
            android:id="@+id/linear_layout"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            >
        </LinearLayout>

    </ScrollView>

        <RelativeLayout

            android:layout_margin="10dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/edittext_rounded_background">

            <EditText
                android:id="@+id/edit_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="10dp"
                android:background="@null"
                android:hint="请输入消息..."
                android:maxLines="5"
                android:padding="10dp"
                android:textColor="@android:color/black" />

            <Button
                android:id="@+id/send_button"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_alignParentEnd="true"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginEnd="10dp"
                android:layout_marginRight="10dp"
                android:background="@drawable/button_rounded_background"
                android:text="发送"
                android:textColor="@android:color/white" />

        </RelativeLayout>
    </LinearLayout>

</FrameLayout>

ScrollView里做聊天框,动态添加用户输入文本和GPT响应的文本

public void setChatInf(String text , int img , int color){
 LinearLayout mLinearLayoutAdd = LayoutInflater.from(getContext()).inflate(R.layout.chat_textview , null).findViewById(R.id.chat_layout);

 mLinearLayoutAdd.setBackground(ContextCompat.getDrawable(getContext(), color));
 imageView = mLinearLayoutAdd.findViewById(R.id.chat_iv);
 imageView.setBackground(getResources().getDrawable(img));
 textView = mLinearLayoutAdd.findViewById(R.id.chat_tv);
 textView.setText(text);
 if (mLinearLayoutAdd.getParent() != null) {
     ((ViewGroup)mLinearLayoutAdd.getParent()).removeView(mLinearLayoutAdd);
 }

 mLinearLayout.addView(mLinearLayoutAdd);
}

chat_textview:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/chat_layout"
    android:gravity="center_vertical"
    android:fillViewport="true"
    android:paddingTop="@dimen/dp_6"
    android:paddingBottom="@dimen/dp_6"
    android:paddingStart="@dimen/dp_11"
    android:paddingEnd="@dimen/dp_11"
    >

    <ImageView
        android:id="@+id/chat_iv"
        android:scaleType="fitCenter"
        android:layout_marginRight="@dimen/dp_14"
        android:layout_width="@dimen/dp_30"
        android:layout_height="@dimen/dp_30"/>

    <TextView
        android:id="@+id/chat_tv"
        android:textColor="@color/black"
        android:textIsSelectable="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

ok,大概长这样

image.png

添加Gson、okHttp依赖——>写一个简单的请求方法

    public void chatCreate(boolean usedP , String url , String urlProxy , String port , String key , String text ,String json, Callback callback) {

        //usedP,是否使用代理
        //url,请求Api地址
        //urlProxy,代理地址
        //port,代理端口号
        //Key,chatGpt的Key
        //text,用户输入问题
        //json,请求json(后续要动态添加上下文,所以这里加了个json参数不定死)
        

        Proxy proxy;
        if (usedP){
            //这里只写了SOCK5代理
             proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(
                    urlProxy, Integer.parseInt(port)));
        }else {
             proxy = null;
        }

        RequestBody requestBodyJson =
                RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
        Request request = new Request.Builder()
                .url(url)
                .post(requestBodyJson)
                .addHeader("content-type", "application/json")
                .addHeader("Authorization", "Bearer " + key)//注意key的格式,官网给的是sk开头的
                .build();
        client = new OkHttpClient.Builder()
                .connectTimeout(10 , TimeUnit.SECONDS)
                .readTimeout(20 , TimeUnit.SECONDS)//如果不采取流式输出要把超时设置长一点
                .proxy(proxy)//添加代理
                .build();
        client.newCall(request).enqueue(callback);
    }

ok,然后写个实体类用于接收返回的数据

public class ChatCompletionChunk2 {
    private String id;
    private String object;
    private long created;
    private String model;
    private List<Choice> choices;

    public String getId() {
        return id;
    }

    public String getObject() {
        return object;
    }

    public long getCreated() {
        return created;
    }

    public String getModel() {
        return model;
    }

    public List<Choice> getChoices() {
        return choices;
    }


    public static class Choice {
        private Delta delta;
        private int index;
        private String finish_reason;

        public Delta getDelta() {
            return delta;
        }

        public int getIndex() {
            return index;
        }

        public String getFinishReason() {
            return finish_reason;
        }
    }

    public static class Delta {
        private String content;

        public String getContent() {
            return content;
        }
    }
}

最后对响应的数据进行解析,完整代码:


public class ChatFragment extends Fragment implements ScreenShotable, View.OnClickListener {

    @Inject
    Api api;

    ChatCompletion chatCompletion;
    ChatCompletionChunk2 chatCompletionChunk2;

    private OkHttpClient client;
    private EditText editText;
    private Button button;
    private LinearLayout mLinearLayout;
    private LinearLayout mLinearLayoutAdd;
    private FrameLayout frameLayout;

    StringBuffer buffer;

    String json;


    private TextView textView;

    private ScrollView scrollView;

    private ImageView imageView;

    @SuppressLint("HandlerLeak")
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 1:
                    //button按钮文字及状态
                    button.setEnabled(true);
                    button.setText((String)msg.obj);
                    //滑动底部
                    scrollView.fullScroll(ScrollView.FOCUS_DOWN);
                    break;
                case 2:
                    //response返回拼接
                    textView.setText(msg.obj.toString());
                    scrollView.fullScroll(ScrollView.FOCUS_DOWN);
                    break;

            }
        }
    };

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate( R.layout.fragment_chat, null);
        return view;
    }

    @Override
    public void onActivityCreated(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        editText = getView().findViewById(R.id.edit_text);
        button = getView().findViewById(R.id.send_button);
        mLinearLayout = getView().findViewById(R.id.linear_layout);
        scrollView = getView().findViewById(R.id.scrollView);
        frameLayout = getView().findViewById(R.id.frag_add);

        mLinearLayoutAdd = LayoutInflater.from(getContext()).inflate(R.layout.chat_textview , null).findViewById(R.id.chat_layout);

        setChatInf("你好,我是Ai小助手,需要帮助吗?" , R.mipmap.chat_img , R.color.c_f2f3f5);

        //流式输出模式,messages因为要添加上下文保持连贯,所以采用动态添加,如果不添加上下文这里mesage可以直接写死
        json = "{"model": "gpt-3.5-turbo", " +
                ""messages": [] , " +
                ""stream" : true}";

        button = getView().findViewById(R.id.send_button);
        button.setOnClickListener(this);
    }

    @SuppressLint("ResourceAsColor")
    @Override
    public void onClick(View v) {
        String message = editText.getText().toString().trim();
        if (!message.isEmpty()) {

            //收起软键盘
            InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
            View view = getActivity().getCurrentFocus();
            if (view != null) {
                imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
            }
            chatResp(message);
        }else {
            Toast.makeText(getContext(), "请输入内容", Toast.LENGTH_SHORT).show();
        }
    }

    @SuppressLint({"ResourceAsColor", "MissingInflatedId"})
    public void sendMsg(final String text) {
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (!text.isEmpty()) {
                    editText.getText().clear();
                    setChatInf(text , R.mipmap.user_img , R.color.c_f7f8fa);

                    setToken(1 , text);

                    button.setEnabled(false);
                    button.setText("别急");
                }
            }
        });
    }

    public void chatResp(final String text){
        sendMsg(text);
        new Thread(new Runnable() {
            @Override
            public void run() {

                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        setChatInf("正在输入中..." , R.mipmap.chat_img , R.color.c_f2f3f5);

                    }
                });
                Api api = new Api();
                SharedPreferences sharedPreferences = getActivity().getSharedPreferences("user", Context.MODE_PRIVATE);

                //这里可以直接传参的
                api.chatCreate(sharedPreferences.getBoolean("radioGroupUsedP" , Constants.usedP) ,
                        Constants.URL_CREATE,
                        sharedPreferences.getString("editTextIp" , Constants.PROXY),
                        sharedPreferences.getString("editTextPort" , String.valueOf(Constants.PORT)),
                        sharedPreferences.getString("Key" , Constants.KEY),
                        text,
                        json,
                        new Callback() {
                    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
                    @Override
                    public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                        //流式输出
                        buffer = new StringBuffer();
                        Gson gson = new Gson();
                        // 获取response输入流
                        InputStream inputStream = response.body().byteStream();
                        // 读取响应数据
                        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
                            String line;
                            while ((line = reader.readLine()) != null) {
                                // 处理每一行数据
                                Log.d("line", "onResponse: " + line);
                                //判断是否返回了数据,去除response前data关键字,不然解析不了
                                if (line.length() > 6) {
                                    Log.d("line.substring", "onResponse: " + line.substring(6));
                                    try {
                                        chatCompletionChunk2 = gson.fromJson(line.substring(5) , ChatCompletionChunk2.class);
                                        Log.d("getContent", "onResponse: " + chatCompletionChunk2.getChoices().get(0).getDelta().getContent());
                                        if (chatCompletionChunk2.getChoices().get(0).getDelta().getContent() != null){
                                            addNewlineAfterPeriod(chatCompletionChunk2.getChoices().get(0).getDelta().getContent());
                                            buffer.append(chatCompletionChunk2.getChoices().get(0).getDelta().getContent());

                                            setMessage(2 , buffer);
                                        }
                                        if (chatCompletionChunk2.getChoices().get(0).getFinishReason() != null) {
                                            break;
                                        }
                                    }catch (Exception e){
                                        e.printStackTrace();
                                        buffer.append("请求有误,请检查key或稍后重试,当然,如果你上下文足够长也会出现,请自行找寻原因,嗯,我懒得写 /doge");

                                        setMessage(2 , buffer);
                                        setToken(2 , buffer.toString());
                                        break;
                                    }
                                }
                            }

                            setMessage(1 , "发送");

                            Log.d("buffer", "onResponse: " + buffer.toString());
                        }
                    }

                    @Override
                    public void onFailure(@NotNull Call call, @NotNull IOException e) {
                        Log.d("onFailure", "onFailure: 请求失败" );

                        setMessage(2 , "请求超时,请检查网络并重试");

                        setMessage(1 , "发送");

                    }

                });

            }
        }).start();
    }


    /**
     * @author liaoqg
     * @date ${YEAR}-${MONTH}-${DAY}
     * 描述
     *      对字符串进行处理
     */

    public String addNewlineAfterPeriod(String str) {
        StringBuilder sb = new StringBuilder();
        boolean periodFound = false;
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c == '.' || c == '。') {
                periodFound = true;
                sb.append(c);
                sb.append('\n');
            } else if (c == '\n') {
                continue;
            } else {
                sb.append(c);
            }
        }
        if (!periodFound) {
            return str;
        }
        return sb.toString();
    }

    /**
     * @author liaoqg
     * @date ${YEAR}-${MONTH}-${DAY}
     * 描述
     *      设置头像和聊天框样式
     */

    public void setChatInf(String text , int img , int color){
        LinearLayout mLinearLayoutAdd = LayoutInflater.from(getContext()).inflate(R.layout.chat_textview , null).findViewById(R.id.chat_layout);

        mLinearLayoutAdd.setBackground(ContextCompat.getDrawable(getContext(), color));
        imageView = mLinearLayoutAdd.findViewById(R.id.chat_iv);
        imageView.setBackground(getResources().getDrawable(img));
        textView = mLinearLayoutAdd.findViewById(R.id.chat_tv);
        textView.setText(text);
        if (mLinearLayoutAdd.getParent() != null) {
            ((ViewGroup)mLinearLayoutAdd.getParent()).removeView(mLinearLayoutAdd);
        }

        mLinearLayout.addView(mLinearLayoutAdd);
    }

    /**
     * @author liaoqg
     * @date ${YEAR}-${MONTH}-${DAY}
     * 描述
     *      handle
     */
    public void setMessage(int what , Object object){
        Message message = Message.obtain();
        message.what = what;
        message.obj = object;
        handler.sendMessage(message);
    }

    /**
     * @author liaoqg
     * @date ${YEAR}-${MONTH}-${DAY}
     * 描述
     *      将上下文信息保存
     *          测试出,只需要用户信息,不需要将chat的回答添加到请求中,也可以保持gpt上下文连贯
     */

    public void setToken(int status , String text){
        try {
            if (Constants.IsTOKEN){
                // 将json字符串转化为json对象
                JSONObject jsonObject = new JSONObject(json);
                // 获取messages字段的json数组对象
                JSONArray messagesArray = jsonObject.getJSONArray("messages");
                if (status == 1){
                    Constants.newMessage  = new JSONObject();
                    Constants.newMessage.put("role", "user");
                    Constants.newMessage.put("content", text);
                }
//            else if (status == 2){
//                Constants.newMessage  = new JSONObject();
//                Constants.newMessage.put("role", "assistant");
//                Constants.newMessage.put("content", text);
//            }
                // 将新的消息对象添加到messages数组中
                messagesArray.put(Constants.newMessage);
                // 更新json对象中的messages字段
                jsonObject.put("messages", messagesArray);
                // 将json对象转化为字符串
                json = jsonObject.toString();
            }else {
                json = "{"model": "gpt-3.5-turbo", " +
                    ""messages": [{"role": "user", "content":  "" + text + ""}] , " +
                    ""stream" : true}";
            }


        } catch (Exception e) {
            e.printStackTrace();
        }

        Log.d("TAG", "setToken: " + json);
    }

}

大功告成,没有什么难度,接收数据的坑很多,有兴趣的可以打印出来好好看看,如果想生成图片也是一样的步骤,官方API文档里面写的很清楚,我就不罗嗦了,ok,上个成品图

www.alltoall.net_870f37e50b7f2411a008a93fc8f4cd5f_EGBUm105qW.gif

ps:如果有自己的代理服务器ip可以在proxy中设置,没有的只能放魔法访问了。现在ip封锁严重,还是建议大家尽量不要选亚洲节点,尤其是港服。

补充

应xdm的要求,特此附上源码链接: github.com/mingzhennan…

这里包括了聊天(chat)和图片(img)的接口实现,Down下来之后需要设置一下key

Ui使用的是Yalantis的侧边菜单栏:github.com/Yalantis/Si…,有兴趣的xdm可以看看